30歳からのプログラミング
30歳無職から独学でプログラミングを開始した人間の記録。
2024-03-09T23:23:04+09:00
numb_86
Hatena::Blog
hatenablog://blog/10328537792366155102
Embulk に入門して Amazon RDS にあるデータを BigQuery に転送する
hatenablog://entry/6801883189089492567
2024-03-09T23:23:04+09:00
2024-03-09T23:23:04+09:00 この記事では、Embulk を使ってデータ転送を行う方法について述べていく。 今回は題材として Amazon RDS から Google Cloud の BigQuery にデータを転送する。Embulk の実行はローカルマシンで行う。 使っている Embulk のバージョンは0.9.25。 0.10や0.11だと異なる手順や設定が必要になると思われるので注意。 事前準備 まずは Amazon RDS のインスタンスを作成する。今回はデータベースの種類は MySQL にした。バージョンは8.0.35。 ローカルマシンから Embulk を実行する都合上、パブリックアクセスを「あり」にしておく必…
<p>この記事では、Embulk を使ってデータ転送を行う方法について述べていく。<br/>
今回は題材として Amazon RDS から Google Cloud の BigQuery にデータを転送する。Embulk の実行はローカルマシンで行う。</p>
<p>使っている Embulk のバージョンは<code>0.9.25</code>。<br/>
<code>0.10</code>や<code>0.11</code>だと異なる手順や設定が必要になると思われるので注意。</p>
<h2 id="事前準備">事前準備</h2>
<p>まずは Amazon RDS のインスタンスを作成する。今回はデータベースの種類は MySQL にした。バージョンは<code>8.0.35</code>。<br/>
ローカルマシンから Embulk を実行する都合上、パブリックアクセスを「あり」にしておく必要がある。そして、セキュリティグループを適切に設定し、ローカルマシンの IP アドレスからのアクセスを許可しておく必要もある。<br/>
作成後は<code>$ docker run -it --rm mysql mysql -u admin -p -h <RDSインスタンスのエンドポイント></code>でインスタンスにログインし、以下のクエリを実行する。</p>
<pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">CREATE</span> DATABASE source_db;
USE source_db;
<span class="synStatement">CREATE</span> <span class="synSpecial">TABLE</span> <span class="synIdentifier">user</span> (
id <span class="synType">INT</span> <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span> AUTO_INCREMENT,
name <span class="synType">VARCHAR</span>(<span class="synConstant">191</span>) <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span>,
PRIMARY KEY (id)
);
<span class="synStatement">INSERT</span> <span class="synSpecial">INTO</span> <span class="synIdentifier">user</span> (name) <span class="synSpecial">VALUES</span> (<span class="synSpecial">'</span><span class="synConstant">Alice</span><span class="synSpecial">'</span>), (<span class="synSpecial">'</span><span class="synConstant">Bob</span><span class="synSpecial">'</span>);
</pre>
<p><code>source_db</code>というデータベースを作り、そのなかに<code>user</code>テーブルを作成、以下のデータを投入した。</p>
<pre class="code bash" data-lang="bash" data-unlink>mysql> SELECT * FROM user;
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
| 2 | Bob |
+----+-------+
2 rows in set (0.01 sec)</pre>
<p>このデータを BigQuery に転送するのが今回の目的。</p>
<p>次に BigQuery 側の準備を行う。<br/>
任意の GCP プロジェクトの BigQuery に<code>destination_dataset</code>というデータセットを用意しておく。テーブルは作らなくてよい。<br/>
RDS だけでなく BigQuery にアクセスする権限も必要なので、サービスアカウントキーを JSON 形式で発行しておく。</p>
<h2 id="ローカルマシンで-Embulk-を実行できるようにする">ローカルマシンで Embulk を実行できるようにする</h2>
<p>今回は Docker を使って Embulk の実行環境を用意する。<br/>
<code>Dockerfile</code>と<code>init_embulk_commands.sh</code>、2 つのファイルを用意する。</p>
<p>まずは<code>Dockerfile</code>。</p>
<pre class="code Dockerfile" data-lang="Dockerfile" data-unlink>FROM openjdk:8-jdk
# embulk.jarをダウンロード
RUN mkdir -p /root/.embulk/bin && \
curl -o /root/.embulk/bin/embulk.jar -L "https://dl.embulk.org/embulk-0.9.25.jar"
# 必要なgemをインストールするためのスクリプトを追加
COPY init_embulk_commands.sh /root/init_embulk_commands.sh
RUN chmod +x /root/init_embulk_commands.sh && /root/init_embulk_commands.sh
WORKDIR /workspace
# embulk実行のためのシェルコマンドを設定
RUN echo '#!/bin/bash\njava -classpath "/root/.embulk/bin/embulk.jar:/root/.embulk/lib/postgresql.jar" org.embulk.cli.Main "$@"' > /usr/local/bin/embulk && \
chmod +x /usr/local/bin/embulk
CMD [ "embulk", "--version" ]</pre>
<p>このなかで<code>init_embulk_commands.sh</code>をコピーしそれを実行している。<br/>
<code>Dockerfile</code>のコメントに書いたようにこれは、必要な gem をインストールするためのシェルスクリプト。<br/>
まずは以下の内容にする。</p>
<pre class="code bash" data-lang="bash" data-unlink>#!/bin/bash
# embulk.jarへのパス
EMBULK_JAR="/root/.embulk/bin/embulk.jar"
# 必要なgemをインストール
java -jar $EMBULK_JAR gem install embulk -v 0.9.25</pre>
<p>この状態で<code>$ docker build -t embulk-image .</code>を実行して Docker image を作成する。</p>
<p>そしてその image から Docker container を作成・実行して Embulk のバージョンが表示されれば、Embulk の実行は成功。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ docker run --rm embulk-image
embulk <span class="synConstant">0.9</span>.<span class="synConstant">25</span>
</pre>
<h2 id="転送設定を記述し実行する">転送設定を記述し実行する</h2>
<p>Embulk を動かすことに成功したので次は、転送元と転送先を設定し、実際に転送を行うようにする。</p>
<p>Embulk は多種多様なデータソースを対象としており、今回対象とした MySQL と BigQuery 以外にも、 Redshift や Snowflake なども対象にすることができる。<br/>
これを可能にしているのがプラグインシステムであり、対応するプラグインさえあれば、転送元や転送先を自由に設定できる。</p>
<p>今回の例では MySQL を転送元、BigQuery を転送先にするので、<code>embulk-input-mysql</code>と<code>embulk-output-bigquery</code>というプラグインが必要になる。</p>
<p><code>init_embulk_commands.sh</code>を以下のように書き換えて、Docker image の作成時に必要なプラグインがインストールされるようにする。</p>
<pre class="code bash" data-lang="bash" data-unlink>#!/bin/bash
# embulk.jarへのパス
EMBULK_JAR="/root/.embulk/bin/embulk.jar"
# 必要なgemをインストール
java -jar $EMBULK_JAR gem install embulk -v 0.9.25
java -jar $EMBULK_JAR gem install embulk-input-mysql
java -jar $EMBULK_JAR gem install jwt -v 2.3.0
java -jar $EMBULK_JAR gem install multipart-post -v 2.1.1
java -jar $EMBULK_JAR gem install public_suffix -v 4.0.7
java -jar $EMBULK_JAR gem install mini_mime -v 1.0.2
java -jar $EMBULK_JAR gem install representable -v 3.0.4
java -jar $EMBULK_JAR gem install embulk-output-bigquery -v 0.6.4</pre>
<p>使いたいプラグイン以外にも様々な gem をインストールしているが、それらは、<code>embulk-output-bigquery</code>の依存関係を解決するために必要な gem。<br/>
必要な gem とそのバージョンは以下の記事を参考にした。<br/>
<a href="https://kkkw.hatenablog.jp/entry/2020/07/30/embulk-input-bigquery%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%A7%E3%82%A8%E3%83%A9%E3%83%BC">embulk-input-bigqueryのインストールでエラー - kikukawa's diary</a></p>
<p>そして<code>Dockerfile</code>の<code>CMD [ "embulk", "--version" ]</code>を<code>CMD ["embulk", "run", "config.yml"]</code>に書き換える。</p>
<p><code>config.yml</code>は Embulk の転送設定を記述するファイルであり、どのデータをどこに転送するのか記述していく。</p>
<p>今回は以下の内容の<code>embulk/config.yml</code>を用意する。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">in</span><span class="synSpecial">:</span>
<span class="synIdentifier">type</span><span class="synSpecial">:</span> mysql
<span class="synIdentifier">host</span><span class="synSpecial">:</span> RDSインスタンスのエンドポイント
<span class="synIdentifier">port</span><span class="synSpecial">:</span> <span class="synConstant">3306</span>
<span class="synIdentifier">user</span><span class="synSpecial">:</span> admin
<span class="synIdentifier">password</span><span class="synSpecial">:</span> <span class="synConstant">"rds password"</span>
<span class="synIdentifier">database</span><span class="synSpecial">:</span> source_db
<span class="synIdentifier">table</span><span class="synSpecial">:</span> user
<span class="synIdentifier">select</span><span class="synSpecial">:</span> <span class="synConstant">"*"</span>
<span class="synIdentifier">out</span><span class="synSpecial">:</span>
<span class="synIdentifier">type</span><span class="synSpecial">:</span> bigquery
<span class="synIdentifier">mode</span><span class="synSpecial">:</span> replace
<span class="synIdentifier">auth_method</span><span class="synSpecial">:</span> json_key
<span class="synIdentifier">json_keyfile</span><span class="synSpecial">:</span> <span class="synConstant">"key.json"</span>
<span class="synIdentifier">project_id</span><span class="synSpecial">:</span> BigQueryのプロジェクト名
<span class="synIdentifier">dataset</span><span class="synSpecial">:</span> destination_dataset
<span class="synIdentifier">table</span><span class="synSpecial">:</span> user
<span class="synIdentifier">auto_create_table</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
</pre>
<p><code>in</code>が転送元で<code>out</code>が転送先。</p>
<p><code>out.json_keyfile</code>にはサービスアカウントキーのパスを記述する。今回は<code>key.json</code>にしたので、<code>config.yml</code>と同じディレクトリ、つまり<code>embulk</code>ディレクトリに<code>key.json</code>として置いておく。</p>
<p>つまり以下の構成になる。</p>
<pre class="code" data-lang="" data-unlink>.
├── Dockerfile
├── embulk
│ ├── config.yml
│ └── key.json
└── init_embulk_commands.sh</pre>
<p>この状態で再び image を作り、そこから container を作成し実行する。container が<code>config.yml</code>や<code>key.json</code>を利用できるように、<code>-v</code>オプションで Volume を作っている。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ docker build -t embulk-image .
$ docker run --rm -v <span class="synConstant">"</span><span class="synPreProc">$(</span><span class="synStatement">pwd</span><span class="synPreProc">)</span><span class="synConstant">/embulk:/workspace"</span> embulk-image
</pre>
<p>そうすると<code>destination_dataset</code>に<code>user</code>テーブルが作られ、RDS に入れておいたのと同じデータが入っている。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20240309/20240309231027.png" width="247" height="56" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20240309/20240309231038.png" width="675" height="128" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
numb_86
Terraform で GitHub の Branch protection rule を定義する
hatenablog://entry/6801883189087799799
2024-03-03T12:34:34+09:00
2024-03-03T12:34:34+09:00 Terraform では GitHub の Branch protection rule を管理する resource が提供されている。この記事ではそれを使って rule を定義する方法について扱う。 動作確認は Terraform のv1.7.4で行った。 Branch protection rule は、指定したブランチに対してルールを設定する仕組みで、pull request に対してレビューを必須にする、特定のテストをパスしないと merge できないようにする、などのルールを設定することができる。 GitHub の UI から設定可能だが、Terraform で管理していくこともで…
<p>Terraform では GitHub の Branch protection rule を管理する resource が提供されている。この記事ではそれを使って rule を定義する方法について扱う。<br/>
動作確認は Terraform の<code>v1.7.4</code>で行った。</p>
<p>Branch protection rule は、指定したブランチに対してルールを設定する仕組みで、pull request に対してレビューを必須にする、特定のテストをパスしないと merge できないようにする、などのルールを設定することができる。</p>
<p>GitHub の UI から設定可能だが、Terraform で管理していくこともできる。<br/>
Terraform には GitHub Provider が提供されており、その中には Branch protection rule を扱う resource もある。そのためそれを使えば、Terraform で Branch protection rule を管理することができる。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fregistry.terraform.io%2Fproviders%2Fintegrations%2Fgithub%2Flatest%2Fdocs%2Fresources%2Fbranch_protection" title="Terraform Registry" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch_protection">registry.terraform.io</a></cite></p>
<p>今回は<code>tf-test</code>というリポジトリを作り、その<code>main</code>ブランチに対して「Status Check にパスしないと merge できない」という rule を作成することにする。</p>
<p>Branch protection rule はリポジトリのページの Settings から見れるが、最初は何の rule も存在しない。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20240303/20240303121947.png" width="1153" height="270" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>まずはリポジトリに<code>.github/workflows/ci.yaml</code>というファイルを追加し、そこに Status Check として行いたい処理(自動テストや型チェックなど)を書いておく。<br/>
これで、記述したトリガー条件に基づいてワークフローが実行されるようになるが、この時点ではまだ Status Check として設定されていない。そのため、このワークフローが失敗しても pull request の merge はできてしまう。</p>
<p>今回は Terraform Cloud で Terraform の state 管理を行うので、Terraform Cloud の設定を行う。<br/>
<code>github-workspace</code>という workspace を作り、その workspace に<code>TF_VAR_GITHUB_TOKEN</code>という Environment Variable を設定する。<code>TF_VAR_GITHUB_TOKEN</code>に必要な権限を持ったトークンを設定することで、Terraform から GitHub を操作できるようになる。</p>
<p>あとは、以下のような Terraform ファイルを書けばよい。</p>
<pre class="code lang-hcl" data-lang="hcl" data-unlink><span class="synType">terraform</span> <span class="synSpecial">{</span>
<span class="synType">cloud</span> <span class="synSpecial">{</span>
<span class="synIdentifier">organization</span> = <span class="synConstant">"your-org-name"</span>
<span class="synType">workspaces</span> <span class="synSpecial">{</span>
<span class="synIdentifier">name</span> = <span class="synConstant">"github-workspace"</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synType">required_providers</span> <span class="synSpecial">{</span>
<span class="synIdentifier">github</span> = <span class="synSpecial">{</span>
<span class="synIdentifier">source</span> = <span class="synConstant">"integrations/github"</span>
<span class="synIdentifier">version</span> = <span class="synConstant">"~> 5.0"</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synType">variable</span> <span class="synConstant">"GITHUB_TOKEN"</span> <span class="synSpecial">{</span>
<span class="synIdentifier">type</span> = string
<span class="synIdentifier">sensitive</span> = <span class="synConstant">true</span>
<span class="synSpecial">}</span>
<span class="synType">provider</span> <span class="synConstant">"github"</span> <span class="synSpecial">{</span>
<span class="synIdentifier">owner</span> = <span class="synConstant">"your github account name"</span>
<span class="synIdentifier">token</span> = var.GITHUB_TOKEN
<span class="synSpecial">}</span>
<span class="synType">resource</span> <span class="synConstant">"github_branch_protection"</span> <span class="synConstant">"example"</span> <span class="synSpecial">{</span>
<span class="synIdentifier">repository_id</span> = <span class="synConstant">"tf-test"</span>
<span class="synIdentifier">pattern</span> = <span class="synConstant">"main"</span>
<span class="synIdentifier">enforce_admins</span> = <span class="synConstant">true</span>
<span class="synType">required_status_checks</span> <span class="synSpecial">{</span>
<span class="synIdentifier">strict</span> = <span class="synConstant">true</span>
<span class="synIdentifier">contexts</span> = <span class="synSpecial">[</span><span class="synConstant">"ci"</span><span class="synSpecial">]</span>
<span class="synSpecial">}</span>
}
</pre>
<p>GitHub の Organization にリポジトリがある場合も、<code>provider "github"</code>に<code>organization</code>ではなく<code>owner</code>を書く。<br/>
今回は自分が作った Organization に<code>tf-test</code>を作ったのだが、上記の記述で問題なく動作した。<br/>
<code>organization</code>という引数もあるのだが、2024/03/03 現在非推奨になっている。<br/>
<a href="https://registry.terraform.io/providers/integrations/github/latest/docs#organization">https://registry.terraform.io/providers/integrations/github/latest/docs#organization</a></p>
<p>この状態で<code>$ terraform init</code>と<code>$ terraform apply</code>を実行すると rule が作られる。</p>
<p>リポジトリのページで確認してみると、<code>main</code>ブランチを対象に以下が有効になっていることが分かる。</p>
<ul>
<li><code>Require status checks to pass before merging</code></li>
<li><code>Require branches to be up to date before merging</code>
<ul>
<li><code>ci</code>という status check が必須になる</li>
</ul>
</li>
<li><code>Do not allow bypassing the above settings</code></li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20240303/20240303122007.png" width="579" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
numb_86
『AWSネットワーク入門 第2版』を読んだ
hatenablog://entry/6801883189069754291
2023-12-25T23:34:36+09:00
2023-12-25T23:34:36+09:00 AWS でネットワークを構築しウェブサーバを公開する手順を解説した入門書。 丁寧かつ簡潔な説明で、初心者でもスムーズに進めていくことができる。 tatsu-zine.com Kubernetes を勉強していた時、ローカル環境である程度動かすことができたので、次はより実践的なことをやろうと思い Amazon EKS を学ぶことにした。 eksctl というコマンドラインツールを使えば簡単に EKS クラスタを作れるらしいと知り、$ eksctl create clusterを実行してみた。 驚くほど簡単にクラスタを作ることができた。そして、何が起きているのか、驚くほど何も分からなかった。 ek…
<p>AWS でネットワークを構築しウェブサーバを公開する手順を解説した入門書。<br/>
丁寧かつ簡潔な説明で、初心者でもスムーズに進めていくことができる。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftatsu-zine.com%2Fbooks%2Faws-network-2ed" title="AWSネットワーク入門 第2版" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tatsu-zine.com/books/aws-network-2ed">tatsu-zine.com</a></cite></p>
<p>Kubernetes を勉強していた時、ローカル環境である程度動かすことができたので、次はより実践的なことをやろうと思い Amazon EKS を学ぶことにした。<br/>
eksctl というコマンドラインツールを使えば簡単に EKS クラスタを作れるらしいと知り、<code>$ eksctl create cluster</code>を実行してみた。<br/>
驚くほど簡単にクラスタを作ることができた。そして、何が起きているのか、驚くほど何も分からなかった。</p>
<p>eksctl が何でもやってくれてしまう。理解した人間が使う分には便利なのだろうが、仕組みを理解したい、基礎を理解したい、という自分のニーズには合っていなかった。<br/>
そして、EKS 以前にそもそも AWS が分からない。eksctl が作成する AWS のリソースについて、何も分からない。</p>
<p>まずは VPC などのネットワーク周りについて理解しないとダメだなと思い、勉強のために手に取ったのが本書。<br/>
今の自分が知りたい内容ではない、今は不要と判断したので、CHAPTER 7 以降は読んでいない。</p>
<p>とにかく初歩的なことから知りたいと思っていたのだが、そのニーズに合っていた。<br/>
必要なことを初歩から説明しており、それでいて必要でないことは説明していないのがよかった。余計な情報や踏み込んだ話題は扱わず、端的にまとまっている。</p>
<p>説明の流れも上手い。<br/>
一度に色々なことをやろうとせず、段階的に進めていく。<br/>
まだ説明していない用語を先に出して初心者を混乱させてしまうようなこともない。</p>
<p>まずは一番ベーシックと思われる EC2 + VPC でウェブサーバを公開できるようになりたい、そのための知識が欲しい、と思い読み始めたが、その目的は十分達成することができた。</p>
numb_86
『Software Design 2022年1月号』の「TerraformではじめるAWS構成管理 インフラを自動で構築&コードで管理」を読んだ
hatenablog://entry/6801883189056035507
2023-11-04T19:14:09+09:00
2023-12-25T23:13:37+09:00 Terraform 何も分からん、取り敢えず概要を知りたいなと思い、手に取った。 雑誌の特集のひとつであるためコンパクトであり(41 ページ)、手っ取り早く雰囲気を掴むのにちょうどよかった。 gihyo.jp 私のような初心者を対象としていると思われ、かなり初歩から丁寧に話が進んでいく。 特に第 1 章が丁寧で、Terraform で何ができるのか、どう使えばいいのか、ということを、実際に AWS のリソースを作りながら簡潔に説明していく。 全体的に実際に手を動かすことを重視しており、具体的なコードを示しながら話が進むので理解しやすかった。 ローカル値やビルトインファンクションを使ったコードの…
<p>Terraform 何も分からん、取り敢えず概要を知りたいなと思い、手に取った。<br/>
雑誌の特集のひとつであるためコンパクトであり(41 ページ)、手っ取り早く雰囲気を掴むのにちょうどよかった。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgihyo.jp%2Fmagazine%2FSD%2Farchive%2F2022%2F202201" title="Software Design 2022年1月号" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://gihyo.jp/magazine/SD/archive/2022/202201">gihyo.jp</a></cite></p>
<p>私のような初心者を対象としていると思われ、かなり初歩から丁寧に話が進んでいく。<br/>
特に第 1 章が丁寧で、Terraform で何ができるのか、どう使えばいいのか、ということを、実際に AWS のリソースを作りながら簡潔に説明していく。</p>
<p>全体的に実際に手を動かすことを重視しており、具体的なコードを示しながら話が進むので理解しやすかった。<br/>
ローカル値やビルトインファンクションを使ったコードの最適化や、TFLint 等のツールの紹介など、より実践的なトピックにも触れている。</p>
<p>タイトルに「AWS構成管理」とあるように AWS を題材にしており、AWS の知識はあることが前提になっている。<br/>
ただ、AWS の話を理解できなかったとしても、「Terraform によるリソースの作成、変更、削除」という本筋の理解に支障はないように思う。</p>
<p>プロバイダのロックファイルや state、Workspace といった用語や概念を知れたのがよかった。存在を知らないと、調べたり興味を持ったりすることもできないから。<br/>
本特集を読む前より少しだけ、Terraform の話を理解できるようになったと思う。</p>
numb_86
Restart Policy と Probe を使った Pod の管理
hatenablog://entry/820878482970043625
2023-09-24T16:00:47+09:00
2023-09-24T16:00:47+09:00 Kubernetes には Restart Policy や Probe という設定や仕組みがある。 これらを適切に使うことで、コンテナが意図した通りに動いているのか、再起動させる必要はないのか、といったことを Kubernetes が継続的にチェックしてくれるようになる。そしてそれだけではなく、チェックした結果に応じて必要な対応も行ってくれるようになる。 開発者は用意した設定を Kubernetes に伝えればよく、そうすればあとは Kubernetes が自律的にコンテナを管理してくれる。 この記事では、Restart Policy や Probe をどのように設定すればよいのか、そしてそ…
<p>Kubernetes には Restart Policy や Probe という設定や仕組みがある。<br/>
これらを適切に使うことで、コンテナが意図した通りに動いているのか、再起動させる必要はないのか、といったことを Kubernetes が継続的にチェックしてくれるようになる。そしてそれだけではなく、チェックした結果に応じて必要な対応も行ってくれるようになる。<br/>
開発者は用意した設定を Kubernetes に伝えればよく、そうすればあとは Kubernetes が自律的にコンテナを管理してくれる。</p>
<p>この記事では、Restart Policy や Probe をどのように設定すればよいのか、そしてその設定の結果どのように動作するのかについて、具体例を示しながら述べていく。</p>
<p>動作確認は以下の環境で行った。</p>
<ul>
<li>Docker Desktop 4.22.1</li>
<li>Kubernetes 1.27.2</li>
</ul>
<h2 id="Restart-Policy">Restart Policy</h2>
<p>Restart Policy は、Pod 内のコンテナが終了したときに再起動するかどうかの設定で、以下の 3 つのいずれかの値を持つ。</p>
<ul>
<li>Always
<ul>
<li>コンテナが終了すると常に再起動する</li>
</ul>
</li>
<li>OnFailure
<ul>
<li>コンテナが異常終了した場合にのみ再起動する</li>
</ul>
</li>
<li>Never
<ul>
<li>コンテナが終了しても再起動しない</li>
</ul>
</li>
</ul>
<p>なお、Deployment で管理している Pod は必ず Always になる。</p>
<p>実際にコンテナを停止させてみて、どのような挙動になるのか見てみる。</p>
<p>以下がサンプルコード。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> http <span class="synStatement">from</span> <span class="synConstant">"http"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> exit <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"node:process"</span><span class="synStatement">;</span>
http
.createServer<span class="synStatement">(function</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> url <span class="synIdentifier">}</span><span class="synStatement">,</span> res<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">switch</span> <span class="synStatement">(</span>url<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">case</span> <span class="synConstant">"/"</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Hello World\n"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">case</span> <span class="synConstant">"/exit-0"</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">500</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Exit by 0\n"</span><span class="synStatement">);</span>
exit<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">case</span> <span class="synConstant">"/exit-1"</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">500</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Exit by 1\n"</span><span class="synStatement">);</span>
exit<span class="synStatement">(</span><span class="synConstant">1</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">case</span> <span class="synConstant">"/oom"</span>: <span class="synIdentifier">{</span>
<span class="synType">const</span> hugeArray <span class="synStatement">=</span> <span class="synIdentifier">[]</span><span class="synStatement">;</span>
<span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> <span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span>
hugeArray.push<span class="synStatement">(</span>i.toString<span class="synStatement">()</span>.repeat<span class="synStatement">(</span><span class="synConstant">1000000</span><span class="synStatement">));</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synStatement">default</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">404</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Not Found\n"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
.listen<span class="synStatement">(</span><span class="synConstant">3000</span><span class="synStatement">);</span>
</pre>
<p>このサンプルコードはウェブサーバを起動するが、各パスにリクエストを送ると以下の結果になる。</p>
<ul>
<li><code>/exit-0</code>
<ul>
<li>レスポンスを返したあとにプロセスを正常終了する</li>
</ul>
</li>
<li><code>/exit-1</code>
<ul>
<li>レスポンスを返したあとにプロセスを異常終了する</li>
</ul>
</li>
<li><code>oom</code>
<ul>
<li>Out of memory(以下、OOM)が発生する</li>
</ul>
</li>
</ul>
<p>このウェブサーバを使って、コンテナがどうなるのか試していく。</p>
<p>上記のコードが動くコンテナイメージを、<code>sample</code>という名前で作る。<br/>
Docker を使ったコンテナイメージの作り方は以前書いた。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2022%2F04%2F11%2F002854" title="Dockerfile に入門して Node.js アプリを作ってみる - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://numb86-tech.hatenablog.com/entry/2022/04/11/002854">numb86-tech.hatenablog.com</a></cite></p>
<p>次はマニフェストファイルを書く。まずは Restart Policy を<code>Never</code>にする。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> v1
<span class="synIdentifier">kind</span><span class="synSpecial">:</span> Pod
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> my-pod
<span class="synIdentifier">labels</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">containers</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> my-container
<span class="synIdentifier">image</span><span class="synSpecial">:</span> sample:latest
<span class="synIdentifier">imagePullPolicy</span><span class="synSpecial">:</span> IfNotPresent
<span class="synIdentifier">ports</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">containerPort</span><span class="synSpecial">:</span> <span class="synConstant">3000</span>
<span class="synIdentifier">resources</span><span class="synSpecial">:</span>
<span class="synIdentifier">limits</span><span class="synSpecial">:</span>
<span class="synIdentifier">memory</span><span class="synSpecial">:</span> 256Mi
<span class="synIdentifier">restartPolicy</span><span class="synSpecial">:</span> Never<span class="synComment"> # Restart Policy</span>
<span class="synPreProc">---</span>
<span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> v1
<span class="synIdentifier">kind</span><span class="synSpecial">:</span> Service
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> my-ser
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">type</span><span class="synSpecial">:</span> NodePort
<span class="synIdentifier">ports</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> my-ser-port
<span class="synIdentifier">port</span><span class="synSpecial">:</span> <span class="synConstant">8099</span>
<span class="synIdentifier">targetPort</span><span class="synSpecial">:</span> <span class="synConstant">3000</span>
<span class="synIdentifier">nodePort</span><span class="synSpecial">:</span> <span class="synConstant">32660</span>
<span class="synIdentifier">selector</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app
</pre>
<p><code>apply</code>コマンドで設定を反映させる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl apply -f manifestfile.yaml
pod/my-pod created
service/my-ser created
</pre>
<p>これで<code>Never</code>で Pod が作られた。</p>
<p>これからコンテナの状態がどのように変化するのかを見ていくが、まずは現在の状態を確認しておく。</p>
<p>コンテナの状態は<code>$ kubectl get pod Podの名前 -o=jsonpath='{.status.containerStatuses}'</code>で見れる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status.containerStatuses}'</span>
[{<span class="synConstant">"containerID"</span>:<span class="synConstant">"docker://0930986742e2e40ab13f09e300745fc1abbb369ea6a0ecf92047acd4a4de9d75"</span>,<span class="synConstant">"image"</span>:<span class="synConstant">"sample:latest"</span>,<span class="synConstant">"imageID"</span>:<span class="synConstant">"docker://sha256:806e7254cc070f24057a0dd4135349d77a5db11b860b4e788969192bf8bf51cc"</span>,<span class="synConstant">"lastState"</span>:{},<span class="synConstant">"name"</span>:<span class="synConstant">"my-container"</span>,<span class="synConstant">"ready"</span>:<span class="synStatement">true</span>,<span class="synConstant">"restartCount"</span>:<span class="synConstant">0</span>,<span class="synConstant">"started"</span>:<span class="synStatement">true</span>,<span class="synConstant">"state"</span>:{<span class="synConstant">"running"</span>:{<span class="synConstant">"startedAt"</span>:<span class="synConstant">"2023-09-20T16:14:30Z"</span>}}}]<span class="synPreProc">```</span>
</pre>
<p>見づらいので <a href="https://jqlang.github.io/jq/">jq</a> で整形する。また、今回見たい情報だけを抜粋して表示する。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"running"</span>: {
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T16:14:30Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">true</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
</pre>
<p><code>state</code>は以下の 3 つのうちのいずれかになる。</p>
<ul>
<li>Running
<ul>
<li>コンテナが正常に動作している</li>
</ul>
</li>
<li>Terminated
<ul>
<li>コンテナが終了した</li>
</ul>
</li>
<li>Waiting
<ul>
<li>Running でも Terminated でもない</li>
</ul>
</li>
</ul>
<p><code>ready</code>はリクエストを処理できる状態であるのかを、<code>restartCount</code>はコンテナが再起動した回数を、それぞれ示している。</p>
<p>つまり、現時点で<code>my-pod</code>内のコンテナは正常に動作しており、リクエストを処理することも可能、そしてまだ一度も再起動していないということが分かる。</p>
<h3 id="exit-0">/exit-0</h3>
<p>まずは<code>/exit-0</code>にリクエストを送ると状態がどのように変化するのか見てみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/exit-0
Exit by <span class="synConstant">0</span>
$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"terminated"</span>: {
<span class="synConstant">"containerID"</span>: <span class="synConstant">"docker://0930986742e2e40ab13f09e300745fc1abbb369ea6a0ecf92047acd4a4de9d75"</span>,
<span class="synConstant">"exitCode"</span>: <span class="synConstant">0</span>,
<span class="synConstant">"finishedAt"</span>: <span class="synConstant">"2023-09-20T16:39:24Z"</span>,
<span class="synConstant">"reason"</span>: <span class="synConstant">"Completed"</span>,
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T16:14:30Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">false</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
</pre>
<p>終了コード<code>0</code>で<code>Terminated</code>となり、<code>ready</code>も<code>false</code>になっている。<code>reason</code>は<code>Completed</code>。</p>
<p>Restart Policy は<code>Never</code>なので、このコンテナはこのまま終了したままであり、再起動されない。</p>
<p>検証を続けるために一度リソースを削除して作り直す。以後、この方法で Pod を作り直していく。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl delete -f manifestfile.yaml
pod <span class="synConstant">"my-pod"</span> deleted
service <span class="synConstant">"my-ser"</span> deleted
$ kubectl apply -f manifestfile.yaml
pod/my-pod created
service/my-ser created
</pre>
<h3 id="exit-1">/exit-1</h3>
<p>次は<code>/exit-1</code>。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/exit-1
Exit by <span class="synConstant">1</span>
$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"terminated"</span>: {
<span class="synConstant">"containerID"</span>: <span class="synConstant">"docker://6ad3a4b88de09b4beea968ae31ccea0b38f018b85b5ac9c89837a79646b22283"</span>,
<span class="synConstant">"exitCode"</span>: <span class="synConstant">1</span>,
<span class="synConstant">"finishedAt"</span>: <span class="synConstant">"2023-09-20T16:44:41Z"</span>,
<span class="synConstant">"reason"</span>: <span class="synConstant">"Error"</span>,
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T16:43:16Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">false</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
</pre>
<p>コンテナが終了(<code>Terminated</code>)しているのは先程と同じだが、終了コードが<code>1</code>に、<code>reason</code>が<code>Error</code>になっている。</p>
<h3 id="oom">/oom</h3>
<p>最後は<code>/oom</code>。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/oom
curl: (<span class="synConstant">52</span>) Empty reply from server
$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"terminated"</span>: {
<span class="synConstant">"containerID"</span>: <span class="synConstant">"docker://a5452537ece6d0042ddcecb62d2270c40938046f320cd47f185a4cd301a95e4a"</span>,
<span class="synConstant">"exitCode"</span>: <span class="synConstant">137</span>,
<span class="synConstant">"finishedAt"</span>: <span class="synConstant">"2023-09-20T16:46:45Z"</span>,
<span class="synConstant">"reason"</span>: <span class="synConstant">"OOMKilled"</span>,
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T16:46:38Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">false</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
</pre>
<p>今度は終了コード<code>137</code>で<code>reason</code>は<code>OOMKilled</code>になっている。</p>
<p><code>/exit-1</code>でも<code>/oom</code>でもコンテナは再起動されず、停止したままになる。</p>
<h3 id="OnFailure-や-Always-による再起動">OnFailure や Always による再起動</h3>
<p>次は Restart Policy を<code>OnFailure</code>にして同様の操作をしてみる。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synStatement">@@ -14,7 +14,7 @@</span><span class="synPreProc"> spec:</span>
resources:
limits:
memory: 256Mi
<span class="synSpecial">- restartPolicy: Always # Restart Policy</span>
<span class="synIdentifier">+ restartPolicy: OnFailure # Restart Policy</span>
---
apiVersion: v1
kind: Service
</pre>
<p>すると、<code>/exit-1</code>と<code>/oom</code>ではコンテナが再起動されることを確認できる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/exit-1
Exit by <span class="synConstant">1</span>
$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"running"</span>: {
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T16:53:36Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {
<span class="synConstant">"terminated"</span>: {
<span class="synConstant">"containerID"</span>: <span class="synConstant">"docker://b34991875c102e2903d4ae07f9abe0d45255d7db41378fefd6671f9a1be6644b"</span>,
<span class="synConstant">"exitCode"</span>: <span class="synConstant">1</span>,
<span class="synConstant">"finishedAt"</span>: <span class="synConstant">"2023-09-20T16:53:35Z"</span>,
<span class="synConstant">"reason"</span>: <span class="synConstant">"Error"</span>,
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T16:53:26Z"</span>
}
},
<span class="synConstant">"ready"</span>: <span class="synStatement">true</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">1</span>
}
$ curl localhost:<span class="synConstant">32660</span>
Hello World
</pre>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/oom
curl: (<span class="synConstant">52</span>) Empty reply from server
$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"running"</span>: {
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T16:55:23Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {
<span class="synConstant">"terminated"</span>: {
<span class="synConstant">"containerID"</span>: <span class="synConstant">"docker://99106019e35dfaf4ad2f1ede6653c686de9a082f229f9d24667793b0712e35f2"</span>,
<span class="synConstant">"exitCode"</span>: <span class="synConstant">137</span>,
<span class="synConstant">"finishedAt"</span>: <span class="synConstant">"2023-09-20T16:55:22Z"</span>,
<span class="synConstant">"reason"</span>: <span class="synConstant">"OOMKilled"</span>,
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T16:55:16Z"</span>
}
},
<span class="synConstant">"ready"</span>: <span class="synStatement">true</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">1</span>
}
$ curl localhost:<span class="synConstant">32660</span>
Hello World
</pre>
<p>終了時の状態が<code>lastState</code>となり、<code>state</code>は<code>Running</code>になっている。そして<code>restartCount</code>がインクリメントされている。<br/>
<code>ready</code>が<code>true</code>なので<code>localhost:32660</code>へのリクエストを正しく処理できている。</p>
<p>だが<code>/exit-0</code>では再起動はされない。これは、終了コードが<code>0</code>、つまり正常終了であるためである。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/exit-0
Exit by <span class="synConstant">0</span>
$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"terminated"</span>: {
<span class="synConstant">"containerID"</span>: <span class="synConstant">"docker://f2bf56baff1f1a5a9a3ce082612aefbc3ffdcefabfe16f1bc5162a8f213df5ae"</span>,
<span class="synConstant">"exitCode"</span>: <span class="synConstant">0</span>,
<span class="synConstant">"finishedAt"</span>: <span class="synConstant">"2023-09-20T16:56:49Z"</span>,
<span class="synConstant">"reason"</span>: <span class="synConstant">"Completed"</span>,
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T16:56:36Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">false</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
$ curl localhost:<span class="synConstant">32660</span>
curl: (<span class="synConstant">52</span>) Empty reply from server
</pre>
<p>コンテナが終了しているので、当然<code>localhost:32660</code>にリクエストを送ってもレスポンスは返ってこない。</p>
<p><code>Always</code>にすると、異常終了に加えて正常終了のときも再起動するようになる。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synStatement">@@ -14,7 +14,7 @@</span><span class="synPreProc"> spec:</span>
resources:
limits:
memory: 256Mi
<span class="synSpecial">- restartPolicy: OnFailure # Restart Policy</span>
<span class="synIdentifier">+ restartPolicy: Always # Restart Policy</span>
---
apiVersion: v1
kind: Service
</pre>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/exit-0
Exit by <span class="synConstant">0</span>
$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"running"</span>: {
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T17:06:13Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {
<span class="synConstant">"terminated"</span>: {
<span class="synConstant">"containerID"</span>: <span class="synConstant">"docker://196f08053ddaaf4381359258e8606bc60319a90eca64ad42af463f905dd250b2"</span>,
<span class="synConstant">"exitCode"</span>: <span class="synConstant">0</span>,
<span class="synConstant">"finishedAt"</span>: <span class="synConstant">"2023-09-20T17:06:12Z"</span>,
<span class="synConstant">"reason"</span>: <span class="synConstant">"Completed"</span>,
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-20T17:06:10Z"</span>
}
},
<span class="synConstant">"ready"</span>: <span class="synStatement">true</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">1</span>
}
$ curl localhost:<span class="synConstant">32660</span>
Hello World
</pre>
<p>コンテナが常に稼働していることを想定している(コンテナが役目を終えて終了することを想定していない)場合、<code>Always</code>にしておけばよいはず。<br/>
そうすれば、何らかの理由でコンテナが終了してしまっても、Kubernetes が再起動してくれる。</p>
<p>しかし状況によっては、終了していないコンテナも再起動したいことがある。例えば、バグ等により正常に動作しなくなってしまったコンテナに対しては、そのままにしておくのではなく再起動させたいかもしれない。<br/>
それにコンテナを再起動させた場合も、すぐにリクエストを受け付けられる状態になるとは限らない。そのようなコンテナに対しては、準備が整うまでリクエストをルーティングしたくないはず。<br/>
同様に、(巨大なファイルを読み込んでいるなどの理由で)一時的にリクエストに応答できなくなったコンテナに対してもルーティングしたくないが、いずれ復帰するので必ずしも再起動させたいわけではない。</p>
<p>Restart Policy だけではこれらのニーズに応えることは難しいが、Probe と組み合わせることで解決できる。</p>
<h2 id="Probe">Probe</h2>
<p>Probe とは、Kubernetes がコンテナに対して行う診断のこと。<br/>
定期的に診断を実行し、問題があれば必要な対応も自動的に行ってくれる。</p>
<p>複数の診断方法が用意されているが、今回はコンテナに HTTP GET リクエストを送る方式を使うことにする。</p>
<p>Probe には Liveness Probe、Startup Probe、Readiness Probe の 3 種類があり、目的によって使い分ける。</p>
<h2 id="Liveness-Probe">Liveness Probe</h2>
<p>Liveness Probe は、コンテナが正常に稼働しているかを診断する。<br/>
診断の結果、「終了こそしていないが正常に稼働していない」と判断された場合、Kubernetes はそのコンテナを終了させる。<br/>
注意しなければならないのは、あくまでも終了させるだけだということ。再起動するかどうかは Restart Policy によって決まる。<br/>
Restart Policy が<code>OnFailure</code>か<code>Always</code>なら再起動するが、<code>Never</code>では再起動せず終了したままになる。</p>
<p>Probe も、マニフェストファイルに書き足すことで設定できる。また、動作確認の都合上、Restart Policy は<code>Never</code>にしておく。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synStatement">@@ -14,7 +14,13 @@</span><span class="synPreProc"> spec:</span>
resources:
limits:
memory: 256Mi
<span class="synSpecial">- restartPolicy: Always # Restart Policy</span>
<span class="synIdentifier">+ livenessProbe:</span>
<span class="synIdentifier">+ httpGet:</span>
<span class="synIdentifier">+ path: /probe</span>
<span class="synIdentifier">+ port: 3000</span>
<span class="synIdentifier">+ periodSeconds: 5</span>
<span class="synIdentifier">+ failureThreshold: 3</span>
<span class="synIdentifier">+ restartPolicy: Never # Restart Policy</span>
---
apiVersion: v1
kind: Service
</pre>
<p><code>httpGet</code>は、指定したポート番号、パスでコンテナに HTTP GET リクエストを送り、レスポンスのステータスコードが 200 ~ 399 なら「コンテナが正常に稼働している」と見做す、という診断方法。<br/>
今回は<code>/probe</code>というパスにリクエストを送る。</p>
<p><code>periodSeconds</code>は Probe を実行する頻度を秒数で指定する。なのでこの例では<code>5</code>秒毎にコンテナにリクエストを送る。</p>
<p><code>failureThreshold</code>はリトライ回数で、この回数まで Probe を試みる。今回は<code>3</code>を指定しているので、<code>3</code>回連続で Probe に失敗すると、「コンテナが正常に稼働していない」と見做され、コンテナは終了させられる。</p>
<p>他にも設定項目があるので、詳細は公式ドキュメントを参照。各項目のデフォルト値や制限なども書かれている。<br/>
<a href="https://kubernetes.io/ja/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes">Liveness Probe、Readiness ProbeおよびStartup Probeを使用する | Kubernetes</a></p>
<p>コンテナでは以下のコードを動かす。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> http <span class="synStatement">from</span> <span class="synConstant">"http"</span><span class="synStatement">;</span>
<span class="synType">const</span> startTime <span class="synStatement">=</span> performance.now<span class="synStatement">();</span>
<span class="synType">let</span> isEnable <span class="synStatement">=</span> <span class="synConstant">true</span><span class="synStatement">;</span>
http
.createServer<span class="synStatement">(function</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> url <span class="synIdentifier">}</span><span class="synStatement">,</span> res<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">switch</span> <span class="synStatement">(</span>url<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">case</span> <span class="synConstant">"/"</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Hello World\n"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">case</span> <span class="synConstant">"/probe"</span>: <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>isEnable<span class="synStatement">)</span> <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Success\n"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
<span class="synConstant">`Probe is success. </span><span class="synSpecial">${Math</span>.floor(
(performance.now() - startTime) / <span class="synConstant">1000</span>
)<span class="synSpecial">}</span><span class="synConstant"> seconds have passed since the process started.`</span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span> <span class="synStatement">else</span> <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">500</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Failure\n"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
<span class="synConstant">`Probe is failure. </span><span class="synSpecial">${Math</span>.floor(
(performance.now() - startTime) / <span class="synConstant">1000</span>
)<span class="synSpecial">}</span><span class="synConstant"> seconds have passed since the process started.`</span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">case</span> <span class="synConstant">"/enable"</span>: <span class="synIdentifier">{</span>
isEnable <span class="synStatement">=</span> <span class="synConstant">true</span><span class="synStatement">;</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Enable probe path\n"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"Enabled"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">case</span> <span class="synConstant">"/disable"</span>: <span class="synIdentifier">{</span>
isEnable <span class="synStatement">=</span> <span class="synConstant">false</span><span class="synStatement">;</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Disable probe path\n"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"Disabled"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">default</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">404</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Not Found\n"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
.listen<span class="synStatement">(</span><span class="synConstant">3000</span><span class="synStatement">);</span>
</pre>
<p>Probe からのリクエストを受け付ける<code>/probe</code>というパスを用意した。<br/>
初期状態では<code>/probe</code>は<code>200</code>を返すが、<code>/disable</code>にリクエストを送るとそれ以降、<code>/probe</code>は<code>500</code>を返すようになる。<code>/enable</code>にリクエストを送ると、それ以降の<code>/probe</code>へのリクエストは<code>200</code>を返すようになる。</p>
<p>上記コードを動かすコンテナのイメージを<code>sample</code>としてビルドした上で、apply を行う。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl apply -f manifestfile.yaml
pod/my-pod created
service/my-ser created
</pre>
<p>これで既に Liveness Probe が実行されているはずなので、ログを見てみる。Pod のログは<code>$ kubectl logs Podの名前</code>で見れる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl logs my-pod
yarn run v<span class="synConstant">1.22</span>.<span class="synConstant">19</span>
$ ts-node-dev index.ts
[INFO] <span class="synConstant">05</span>:<span class="synConstant">02</span>:<span class="synConstant">34</span> ts-node-dev ver. <span class="synConstant">2.0</span>.<span class="synConstant">0</span> (using ts-node ver. <span class="synConstant">10.9</span>.<span class="synConstant">1</span>, typescript ver. <span class="synConstant">5.2</span>.<span class="synConstant">2</span>)
Probe is success. <span class="synConstant">3</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">8</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">13</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">18</span> seconds have passed since the process started.
</pre>
<p><code>5</code>秒毎に<code>/probe</code>へのリクエストが発生していることが分かる。</p>
<p><code>/disable</code>へリクエストを送った数秒後に<code>/enable</code>にリクエストを送ってみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/<span class="synStatement">disable</span>
Disable probe path
$ curl localhost:<span class="synConstant">32660</span>/<span class="synStatement">enable</span>
Enable probe path
</pre>
<p>再びログを見てみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl logs my-pod
yarn run v<span class="synConstant">1.22</span>.<span class="synConstant">19</span>
$ ts-node-dev index.ts
[INFO] <span class="synConstant">05</span>:<span class="synConstant">02</span>:<span class="synConstant">34</span> ts-node-dev ver. <span class="synConstant">2.0</span>.<span class="synConstant">0</span> (using ts-node ver. <span class="synConstant">10.9</span>.<span class="synConstant">1</span>, typescript ver. <span class="synConstant">5.2</span>.<span class="synConstant">2</span>)
Probe is success. <span class="synConstant">3</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">8</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">13</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">18</span> seconds have passed since the process started.
Disabled
Probe is failure. <span class="synConstant">23</span> seconds have passed since the process started.
Probe is failure. <span class="synConstant">28</span> seconds have passed since the process started.
Enabled
Probe is success. <span class="synConstant">33</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">38</span> seconds have passed since the process started.
</pre>
<p><code>2</code>回連続で失敗しているが、<code>3</code>回目で成功したため、コンテナは終了することなく稼働し続けている。そして当然、Probe はその後も行われる。</p>
<p>もう一度<code>/disable</code>にリクエストを送り、今度はそのままにしてみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/<span class="synStatement">disable</span>
Disable probe path
</pre>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl logs my-pod
yarn run v<span class="synConstant">1.22</span>.<span class="synConstant">19</span>
$ ts-node-dev index.ts
[INFO] <span class="synConstant">05</span>:<span class="synConstant">02</span>:<span class="synConstant">34</span> ts-node-dev ver. <span class="synConstant">2.0</span>.<span class="synConstant">0</span> (using ts-node ver. <span class="synConstant">10.9</span>.<span class="synConstant">1</span>, typescript ver. <span class="synConstant">5.2</span>.<span class="synConstant">2</span>)
Probe is success. <span class="synConstant">3</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">8</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">13</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">18</span> seconds have passed since the process started.
Disabled
Probe is failure. <span class="synConstant">23</span> seconds have passed since the process started.
Probe is failure. <span class="synConstant">28</span> seconds have passed since the process started.
Enabled
Probe is success. <span class="synConstant">33</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">38</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">43</span> seconds have passed since the process started.
Disabled
Probe is failure. <span class="synConstant">48</span> seconds have passed since the process started.
Probe is failure. <span class="synConstant">53</span> seconds have passed since the process started.
Probe is failure. <span class="synConstant">58</span> seconds have passed since the process started.
</pre>
<p><code>3</code>回連続で失敗したため、このコンテナは Kubernetes によって終了させられた。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"terminated"</span>: {
<span class="synConstant">"containerID"</span>: <span class="synConstant">"docker://5d798fd0a665c1b2dbd008f55cfb1cd132a1f82b1f5d9c6cc7e862eb40c6a8f7"</span>,
<span class="synConstant">"exitCode"</span>: <span class="synConstant">1</span>,
<span class="synConstant">"finishedAt"</span>: <span class="synConstant">"2023-09-23T05:03:34Z"</span>,
<span class="synConstant">"reason"</span>: <span class="synConstant">"Error"</span>,
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-23T05:02:34Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">false</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
</pre>
<p>終了コードが<code>1</code>なので、既述の通り Restart Policy が<code>Always</code>か<code>OnFailure</code>ならコンテナは再起動する。</p>
<h2 id="Startup-Probe">Startup Probe</h2>
<p>Liveness Probe を使うことでコンテナが正常に稼働しているかチェックできる。<br/>
しかし、初期化処理に時間が掛かり、Liveness Probe に応答できるようになるまでに時間が掛かるコンテナの場合は、どうしたらよいだろうか。<br/>
例えば先程の例では<code>5</code>秒毎に Liveness Probe を実行していたが、初期化処理に<code>30</code>秒から<code>60</code>秒ほど掛かる場合、Liveness Probe は必ず失敗し、コンテナが終了してしまう。再起動させたところでまた、コンテナの準備が整う前に Liveness Probe が実行され、それに失敗して再びコンテナは終了してしまう。<br/>
設定によって Liveness Probe の開始を遅らせることもできるが、その場合、一体何秒遅らせればよいのだろうか。余裕を持って<code>90</code>秒くらいにしておけば、終了と再起動のループに陥ることはないだろう。しかし<code>30</code>秒程度で準備が整うこともあり、その場合はコンテナの準備が整い次第すぐに Liveness Probe を始めたい。</p>
<p>Liveness Probe に Startup Probe を組み合わせることで、上記のような課題を解決できる。</p>
<p>Startup Probe は、コンテナの起動が正常に完了したかを診断する。<br/>
Liveness Probe と同様、<code>failureThreshold</code>に指定した回数だけ連続で失敗すると、コンテナは終了する。終了コードは<code>1</code>なので、Restart Policy が<code>Always</code>か<code>OnFailure</code>ならコンテナは再起動する。</p>
<p>Startup Probe は Liveness Probe とは違い、一度成功すればそれ以降は実行されない。<br/>
そしてこれが重要な点だが、Startup Probe が成功するまでは、他の Probe (Liveness Probe や、後述する Readiness Probe)は実行されなくなる。<br/>
つまり、コンテナが起動を開始した直後は Startup Probe によって診断を行い、それが成功した後は Liveness Probe によって継続的な診断を行う、ということが可能になる。</p>
<p>先程のマニフェストファイルに Startup Probe の記述を追加して、試してみる。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synStatement">@@ -20,6 +20,12 @@</span><span class="synPreProc"> spec:</span>
port: 3000
periodSeconds: 5
failureThreshold: 3
<span class="synIdentifier">+ startupProbe:</span>
<span class="synIdentifier">+ httpGet:</span>
<span class="synIdentifier">+ path: /probe</span>
<span class="synIdentifier">+ port: 3000</span>
<span class="synIdentifier">+ periodSeconds: 15</span>
<span class="synIdentifier">+ failureThreshold: 6</span>
restartPolicy: Never # Restart Policy
---
apiVersion: v1
</pre>
<p><code>15</code>秒間隔で実行し、<code>6</code>回連続で失敗したらコンテナを終了させるようにしている。<br/>
つまり、起動開始から<code>90</code>秒の猶予がある。それまでに Startup Probe が成功しなかった場合、「コンテナの起動を完了させることができなかった」と見做し、Kubernetes によってコンテナは終了させられる。</p>
<p>Startup Probe に成功した場合、それ以降は Startup Probe は実行されなくなり、Liveness Probe の実行が開始される。<br/>
Liveness Probe の設定は変えていないので、先程と同様<code>5</code>秒間隔で継続的に実行される。</p>
<p>コンテナで動かすコードは以下。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> http <span class="synStatement">from</span> <span class="synConstant">"http"</span><span class="synStatement">;</span>
<span class="synType">const</span> startTime <span class="synStatement">=</span> performance.now<span class="synStatement">();</span>
<span class="synType">let</span> isEnable <span class="synStatement">=</span> <span class="synConstant">false</span><span class="synStatement">;</span>
setTimeout<span class="synStatement">(()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
isEnable <span class="synStatement">=</span> <span class="synConstant">true</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synConstant">30</span> * <span class="synConstant">1000</span><span class="synStatement">);</span>
http
.createServer<span class="synStatement">(function</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> url <span class="synIdentifier">}</span><span class="synStatement">,</span> res<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>isEnable<span class="synStatement">)</span> <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">500</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Failure\n"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
<span class="synConstant">`Probe is failure. </span><span class="synSpecial">${Math</span>.floor(
(performance.now() - startTime) / <span class="synConstant">1000</span>
)<span class="synSpecial">}</span><span class="synConstant"> seconds have passed since the process started.`</span>
<span class="synStatement">);</span>
<span class="synStatement">return;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">switch</span> <span class="synStatement">(</span>url<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">case</span> <span class="synConstant">"/"</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Hello World\n"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">case</span> <span class="synConstant">"/probe"</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Success\n"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
<span class="synConstant">`Probe is success. </span><span class="synSpecial">${Math</span>.floor(
(performance.now() - startTime) / <span class="synConstant">1000</span>
)<span class="synSpecial">}</span><span class="synConstant"> seconds have passed since the process started.`</span>
<span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">default</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">404</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Not Found\n"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
.listen<span class="synStatement">(</span><span class="synConstant">3000</span><span class="synStatement">);</span>
</pre>
<p>初期状態だと全てのリクエストに対してステータスコード<code>500</code>を返すようになっている。<br/>
そして<code>30</code>秒経過すると<code>/</code>と<code>/probe</code>へのリクエストに対してステータスコード<code>200</code>を返すようになる。</p>
<p>上記コードを動かすコンテナイメージをビルドして、apply する。</p>
<p>コンテナのステータスを確認してみると、<code>Running</code>ではあるのだが、<code>ready</code>が<code>false</code>になっている。つまり、コンテナは動作しているものの、リクエストを受け付けられる状態ではないという扱いになっている。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"running"</span>: {
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-23T07:01:42Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">false</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
</pre>
<p>コンテナが Ready ではない場合、そのコンテナを管理している Pod は Service の Endpoints から外される。つまり、この Pod にリクエストがルーティングされることはなくなる。</p>
<p><code>$ kubectl describe endpoints サービスの名前</code>で Endpoints の詳細を確認できるので、見てみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl describe endpoints my-ser
Name: my-ser
Namespace: default
Labels: <span class="synStatement"><</span>none<span class="synStatement">></span>
Annotations: <span class="synStatement"><</span>none<span class="synStatement">></span>
Subsets:
Addresses: <span class="synStatement"><</span>none<span class="synStatement">></span>
NotReadyAddresses: <span class="synConstant">10.1</span>.<span class="synConstant">1.1</span>
Ports:
Name Port Protocol
---- ---- --------
my-ser-port <span class="synConstant">3000</span> TCP
Events: <span class="synStatement"><</span>none<span class="synStatement">></span>
</pre>
<p><code>my-pod</code>の IP アドレス(<code>10.1.1.1</code>)は<code>NotReadyAddresses</code>になっている。<br/>
今回の例では他に Pod がないので、<code>my-ser</code>がルーティングできる Pod はひとつもない。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get endpoints my-ser
NAME ENDPOINTS AGE
my-ser 11s
</pre>
<p>なので、クラスタの外からリクエストを送ってもレスポンスを得られない(<code>500</code>エラーを得ることもできない)。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>
curl: (<span class="synConstant">52</span>) Empty reply from server
</pre>
<p>Endpoints については以下の記事に書いている。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2023%2F09%2F19%2F211324" title="Docker Desktop を使って学ぶ Kubernetes の基本的な仕組み - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://numb86-tech.hatenablog.com/entry/2023/09/19/211324">numb86-tech.hatenablog.com</a></cite></p>
<p><code>30</code>秒経過すると成功するはずなので確認してみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"running"</span>: {
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-23T07:01:42Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">true</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
$ kubectl describe endpoints my-ser
Name: my-ser
Namespace: default
Labels: <span class="synStatement"><</span>none<span class="synStatement">></span>
Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2023-09-23T07:<span class="synConstant">02</span>:27Z
Subsets:
Addresses: <span class="synConstant">10.1</span>.<span class="synConstant">1.1</span>
NotReadyAddresses: <span class="synStatement"><</span>none<span class="synStatement">></span>
Ports:
Name Port Protocol
---- ---- --------
my-ser-port <span class="synConstant">3000</span> TCP
Events: <span class="synStatement"><</span>none<span class="synStatement">></span>
$ kubectl get endpoints my-ser
NAME ENDPOINTS AGE
my-ser <span class="synConstant">10.1</span>.<span class="synConstant">1.1</span>:<span class="synConstant">3000</span> 58s
</pre>
<p>Ready になっており、Endpoints に追加されている。</p>
<p>これで、クラスタの外からのリクエストに対応できるようになった。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>
Hello World
</pre>
<p>Pod のログを確認してみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl logs my-pod
yarn run v<span class="synConstant">1.22</span>.<span class="synConstant">19</span>
$ ts-node-dev index.ts
[INFO] <span class="synConstant">07</span>:<span class="synConstant">01</span>:<span class="synConstant">43</span> ts-node-dev ver. <span class="synConstant">2.0</span>.<span class="synConstant">0</span> (using ts-node ver. <span class="synConstant">10.9</span>.<span class="synConstant">1</span>, typescript ver. <span class="synConstant">5.2</span>.<span class="synConstant">2</span>)
Probe is failure. <span class="synConstant">13</span> seconds have passed since the process started.
Probe is failure. <span class="synConstant">28</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">43</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">48</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">53</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">58</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">63</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">68</span> seconds have passed since the process started.
</pre>
<p>最初の<code>3</code>回の Probe は、Startup Probe によるもの。<code>15</code>秒毎に実行されている。<br/>
そして<code>3</code>回目で成功するので、それ以降は Startup Probe は行われず、今度は Liveness Probe が実行されるようになる。<br/>
<code>4</code>回目以降の Probe が Liveness Probe だが、<code>5</code>秒毎に実行されていることを確認できる。</p>
<h2 id="Readiness-Probe">Readiness Probe</h2>
<p>Startup Probe と Liveness Probe で、コンテナの起動は正常に完了したか、コンテナは正常に稼働しているかを、診断できるようになった。<br/>
だが、正常に稼働しているコンテナであっても、負荷が強くなるなどの理由で、一時的にリクエストに応答できなくなることはあり得る。<br/>
このような、「コンテナを再起動させたいわけではないがリクエストは受け付けられない」という状態を検知するための Probe が、Readiness Probe である。</p>
<p>Readiness Probe は<code>failureThreshold</code>で指定した回数連続で失敗すると、そのコンテナを管理している Pod が Service の Endpoints から外される。<br/>
その後も Readiness Probe は定期的に実行されており、成功すると Endpoints に加えられ、再びその Pod に対してリクエストがルーティングされるようになる。</p>
<p>以下が Readiness Probe の設定の例。今回は Liveness Probe と Startup Probe を外し、診断を毎秒実行、<code>1</code>回でも失敗すればルーティングしないようにしている。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synStatement">@@ -14,18 +14,12 @@</span><span class="synPreProc"> spec:</span>
resources:
limits:
memory: 256Mi
<span class="synSpecial">- livenessProbe:</span>
<span class="synIdentifier">+ readinessProbe:</span>
httpGet:
path: /probe
port: 3000
<span class="synSpecial">- periodSeconds: 5</span>
<span class="synSpecial">- failureThreshold: 3</span>
<span class="synSpecial">- startupProbe:</span>
<span class="synSpecial">- httpGet:</span>
<span class="synSpecial">- path: /probe</span>
<span class="synSpecial">- port: 3000</span>
<span class="synSpecial">- periodSeconds: 15</span>
<span class="synSpecial">- failureThreshold: 6</span>
<span class="synIdentifier">+ periodSeconds: 1</span>
<span class="synIdentifier">+ failureThreshold: 1</span>
restartPolicy: Never # Restart Policy
---
apiVersion: v1
</pre>
<p>コンテナでは以下のコードを動かす。<code>/heavy</code>にリクエストすると<code>10</code>秒間処理が停止し、その間はあらゆるリクエストに応答できなくなる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> http <span class="synStatement">from</span> <span class="synConstant">"http"</span><span class="synStatement">;</span>
<span class="synType">const</span> startTime <span class="synStatement">=</span> performance.now<span class="synStatement">();</span>
<span class="synStatement">function</span> sleep<span class="synStatement">(</span>ms: <span class="synType">number</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> startTime <span class="synStatement">=</span> performance.now<span class="synStatement">();</span>
<span class="synStatement">while</span> <span class="synStatement">(</span>performance.now<span class="synStatement">()</span> - startTime <span class="synStatement"><</span> ms<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
http
.createServer<span class="synStatement">(function</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> url <span class="synIdentifier">}</span><span class="synStatement">,</span> res<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">switch</span> <span class="synStatement">(</span>url<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">case</span> <span class="synConstant">"/"</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Hello World\n"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">case</span> <span class="synConstant">"/probe"</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Success\n"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
<span class="synConstant">`Probe is success. </span><span class="synSpecial">${Math</span>.floor(
(performance.now() - startTime) / <span class="synConstant">1000</span>
)<span class="synSpecial">}</span><span class="synConstant"> seconds have passed since the process started.`</span>
<span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">case</span> <span class="synConstant">"/heavy"</span>: <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
<span class="synConstant">`Start heavy process. </span><span class="synSpecial">${Math</span>.floor(
(performance.now() - startTime) / <span class="synConstant">1000</span>
)<span class="synSpecial">}</span><span class="synConstant"> seconds have passed since the process started.`</span>
<span class="synStatement">);</span>
sleep<span class="synStatement">(</span><span class="synConstant">10</span> * <span class="synConstant">1000</span><span class="synStatement">);</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Heavy path\n"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
<span class="synConstant">`Finished heavy process. </span><span class="synSpecial">${Math</span>.floor(
(performance.now() - startTime) / <span class="synConstant">1000</span>
)<span class="synSpecial">}</span><span class="synConstant"> seconds have passed since the process started.`</span>
<span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">default</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">404</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Not Found\n"</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
.listen<span class="synStatement">(</span><span class="synConstant">3000</span><span class="synStatement">);</span>
</pre>
<p>今回もコンテナイメージをビルドして apply する。</p>
<p>ステータスを確認してみると、<code>Running</code>であり、Ready である。問題なく稼働している。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"running"</span>: {
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-23T10:00:32Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">true</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
$ kubectl get endpoints my-ser
NAME ENDPOINTS AGE
my-ser <span class="synConstant">10.1</span>.<span class="synConstant">1.7</span>:<span class="synConstant">3000</span> 6s
</pre>
<p><code>/heavy</code>にリクエストを送り、コンテナがリクエストに応答できない状態にしてみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>/heavy
^C
</pre>
<p><code>Ctrl + c</code>ですぐに処理を中断したあと、再びコンテナや Endpoints の状態を確認してみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"running"</span>: {
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-23T10:00:32Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">false</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
$ kubectl get endpoints my-ser
NAME ENDPOINTS AGE
my-ser 20s
</pre>
<p><code>Running</code>ではあるものの Ready ではないと見做され、Endpoints から Pod の IP アドレスが外れている。</p>
<p><code>10</code>秒経過すると<code>/heavy</code>の処理が終わり、再びリクエストを受け付けられる状態になる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pod my-pod -o=jsonpath=<span class="synConstant">'{.status}'</span> <span class="synStatement">|</span> jq <span class="synConstant">'.containerStatuses[] | {state, lastState, ready, restartCount}'</span>
{
<span class="synConstant">"state"</span>: {
<span class="synConstant">"running"</span>: {
<span class="synConstant">"startedAt"</span>: <span class="synConstant">"2023-09-23T10:00:32Z"</span>
}
},
<span class="synConstant">"lastState"</span>: {},
<span class="synConstant">"ready"</span>: <span class="synStatement">true</span>,
<span class="synConstant">"restartCount"</span>: <span class="synConstant">0</span>
}
$ kubectl get endpoints my-ser
NAME ENDPOINTS AGE
my-ser <span class="synConstant">10.1</span>.<span class="synConstant">1.7</span>:<span class="synConstant">3000</span> 28s
</pre>
<p>Pod のログを見てみると、Probe が毎秒実行されていることが分かる。<br/>
そして<code>/heavy</code>の処理が始まると(Probe からの)後続のリクエストが捌かれずに滞留し、<code>10</code>秒経過後に溜まっていたリクエストが一気に処理されている。<br/>
この<code>10</code>秒間が、Ready ではなかった期間となる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl logs my-pod
yarn run v<span class="synConstant">1.22</span>.<span class="synConstant">19</span>
$ ts-node-dev index.ts
[INFO] <span class="synConstant">10</span>:<span class="synConstant">00</span>:<span class="synConstant">33</span> ts-node-dev ver. <span class="synConstant">2.0</span>.<span class="synConstant">0</span> (using ts-node ver. <span class="synConstant">10.9</span>.<span class="synConstant">1</span>, typescript ver. <span class="synConstant">5.2</span>.<span class="synConstant">2</span>)
Probe is success. <span class="synConstant">0</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">0</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">1</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">2</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">3</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">4</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">5</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">6</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">7</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">8</span> seconds have passed since the process started.
Start heavy process. <span class="synConstant">9</span> seconds have passed since the process started.
Finished heavy process. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">19</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">20</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">21</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">22</span> seconds have passed since the process started.
Probe is success. <span class="synConstant">23</span> seconds have passed since the process started.
</pre>
<p>Readiness Probe を適切に設定することで、システムを安定的に稼働させることができる。</p>
<p>例えば、今回と同じコードを 2 つの Pod で動かすケースを想定してみる。</p>
<p>まず Readiness Probe を設定しない場合。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> apps/v1
<span class="synIdentifier">kind</span><span class="synSpecial">:</span> Deployment
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> my-dep
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">selector</span><span class="synSpecial">:</span>
<span class="synIdentifier">matchLabels</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app
<span class="synIdentifier">replicas</span><span class="synSpecial">:</span> <span class="synConstant">2</span>
<span class="synIdentifier">template</span><span class="synSpecial">:</span>
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> my-pod
<span class="synIdentifier">labels</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">containers</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> my-container
<span class="synIdentifier">image</span><span class="synSpecial">:</span> sample:latest
<span class="synIdentifier">imagePullPolicy</span><span class="synSpecial">:</span> IfNotPresent
<span class="synIdentifier">ports</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">containerPort</span><span class="synSpecial">:</span> <span class="synConstant">3000</span>
<span class="synIdentifier">resources</span><span class="synSpecial">:</span>
<span class="synIdentifier">limits</span><span class="synSpecial">:</span>
<span class="synIdentifier">memory</span><span class="synSpecial">:</span> 256Mi
<span class="synPreProc">---</span>
<span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> v1
<span class="synIdentifier">kind</span><span class="synSpecial">:</span> Service
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> my-ser
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">type</span><span class="synSpecial">:</span> NodePort
<span class="synIdentifier">ports</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> my-ser-port
<span class="synIdentifier">port</span><span class="synSpecial">:</span> <span class="synConstant">8099</span>
<span class="synIdentifier">targetPort</span><span class="synSpecial">:</span> <span class="synConstant">3000</span>
<span class="synIdentifier">nodePort</span><span class="synSpecial">:</span> <span class="synConstant">32660</span>
<span class="synIdentifier">selector</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app
</pre>
<p>この内容でクラスタを動かしているときに、<code>req.sh</code>という名前の以下のシェルスクリプトを実行する。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink><span class="synPreProc">#!/bin/zsh</span>
<span class="synComment"># localhost:32660/heavy にリクエストを送るが、結果を待たずに次へ進む</span>
curl -s localhost:<span class="synConstant">32660</span>/heavy <span class="synStatement">></span> /dev/null <span class="synConstant">2</span><span class="synStatement">></span>&<span class="synConstant">1</span> &
<span class="synComment"># 2 秒間待機</span>
sleep <span class="synConstant">2</span>
<span class="synComment"># 200回のリクエストを実行</span>
<span class="synStatement">for</span> i <span class="synStatement">in</span> {<span class="synConstant">1</span>..<span class="synConstant">200</span>}; <span class="synStatement">do</span>
<span class="synStatement">echo</span> <span class="synPreProc">$i</span>
<span class="synStatement">done</span> <span class="synStatement">|</span> xargs -n <span class="synConstant">1</span> -P <span class="synConstant">10</span> -I {} sh -c <span class="synConstant">'curl -s --max-time 0.1 localhost:32660 > /dev/null 2>&1 && echo success || echo failure'</span> <span class="synStatement">>></span> results.txt
<span class="synComment"># 成功と失敗の回数をカウント</span>
success_count=<span class="synPreProc">$(</span>grep -c <span class="synConstant">"success"</span> results.txt<span class="synPreProc">)</span>
failure_count=<span class="synPreProc">$(</span>grep -c <span class="synConstant">"failure"</span> results.txt<span class="synPreProc">)</span>
<span class="synComment"># 結果の表示</span>
<span class="synStatement">echo</span> <span class="synConstant">"成功した回数: </span><span class="synPreProc">$success_count</span><span class="synConstant">"</span>
<span class="synStatement">echo</span> <span class="synConstant">"失敗した回数: </span><span class="synPreProc">$failure_count</span><span class="synConstant">"</span>
<span class="synComment"># results.txt ファイルを削除</span>
rm results.txt
</pre>
<p><code>/heavy</code>にリクエストを送り、その<code>2</code>秒後に<code>/</code>に対して<code>200</code>回リクエストを送る。<code>/</code>へのリクエストは<code>0.1</code>秒でタイムアウトするようにしている。<br/>
そして最後に、<code>200</code>回のうち何回成功し何回失敗したかを表示する。</p>
<p>実行結果は以下のようになる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ ./req.sh
成功した回数: <span class="synConstant">102</span>
失敗した回数: <span class="synConstant">98</span>
</pre>
<p>数字は多少前後するが、概ねこれくらいの結果になる。<br/>
Pod が 2 つあるが、そのうちのひとつが<code>/heavy</code>の処理によって<code>0.1</code>秒以内にリクエストに応答できない状態になっているため、大体半分くらいのリクエストが失敗する。</p>
<p>Readiness Probe を設定することで、この問題を解決できる。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synStatement">@@ -22,6 +22,12 @@</span><span class="synPreProc"> spec:</span>
resources:
limits:
memory: 256Mi
<span class="synIdentifier">+ readinessProbe:</span>
<span class="synIdentifier">+ httpGet:</span>
<span class="synIdentifier">+ path: /probe</span>
<span class="synIdentifier">+ port: 3000</span>
<span class="synIdentifier">+ periodSeconds: 1</span>
<span class="synIdentifier">+ failureThreshold: 1</span>
---
apiVersion: v1
kind: Service
</pre>
<p>先程のシェルスクリプトを再び実行すると、今度は全て成功するようになる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ ./req.sh
成功した回数: <span class="synConstant">200</span>
失敗した回数: <span class="synConstant">0</span>
</pre>
<p><code>/heavy</code>のリクエストを受け付けた Pod は Readiness Probe が失敗するため、リクエストがルーティングされなくなる。<br/>
その結果、リクエストにすぐに応答できる状態のもう一方の Pod に、全てのリクエストがルーティングされるようになる。<br/>
そのため、全てのリクエストが成功するのである。</p>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://kubernetes.io/ja/docs/concepts/workloads/pods/pod-lifecycle/">Podのライフサイクル | Kubernetes</a></li>
<li><a href="https://kubernetes.io/ja/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/">Liveness Probe、Readiness ProbeおよびStartup Probeを使用する | Kubernetes</a></li>
</ul>
numb_86
Docker Desktop を使って学ぶ Kubernetes の基本的な仕組み
hatenablog://entry/820878482968026959
2023-09-19T21:13:24+09:00
2023-09-19T21:13:24+09:00 この記事では Docker Desktop 上で Kubernetes クラスタを作り、実際に動かしながら、Kubernetes の基本的な仕組みについて説明していく。 動作確認は以下の環境で行った。 Docker Desktop 4.22.1 Kubernetes 1.27.2 事前準備 Kubernetes の有効化 Docker Desktop のダッシュボードから設定画面を開き、Enable Kubernetesのような項目を有効にすると Kubernetes を使えるようになる。 kubectlコマンドが使えるようになっていれば問題ないはず。 $ kubectl -h kubectl…
<p>この記事では Docker Desktop 上で Kubernetes クラスタを作り、実際に動かしながら、Kubernetes の基本的な仕組みについて説明していく。</p>
<p>動作確認は以下の環境で行った。</p>
<ul>
<li>Docker Desktop 4.22.1</li>
<li>Kubernetes 1.27.2</li>
</ul>
<h2 id="事前準備">事前準備</h2>
<h3 id="Kubernetes-の有効化">Kubernetes の有効化</h3>
<p>Docker Desktop のダッシュボードから設定画面を開き、<code>Enable Kubernetes</code>のような項目を有効にすると Kubernetes を使えるようになる。</p>
<p><code>kubectl</code>コマンドが使えるようになっていれば問題ないはず。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl -h
kubectl controls the Kubernetes cluster manager.
</pre>
<h3 id="コンテナイメージの用意">コンテナイメージの用意</h3>
<p><code>sample</code>という名前で、以下の内容のウェブサーバが動くコンテナイメージを作成しておく。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> http <span class="synStatement">from</span> <span class="synConstant">"http"</span><span class="synStatement">;</span>
http
.createServer<span class="synStatement">(function</span> <span class="synStatement">(</span>_<span class="synStatement">,</span> res<span class="synStatement">)</span> <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Hello World\n"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
.listen<span class="synStatement">(</span><span class="synConstant">3000</span><span class="synStatement">);</span>
</pre>
<p>Docker を使ったコンテナイメージの作り方は以下を参照。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2022%2F04%2F11%2F002854" title="Dockerfile に入門して Node.js アプリを作ってみる - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://numb86-tech.hatenablog.com/entry/2022/04/11/002854">numb86-tech.hatenablog.com</a></cite></p>
<h2 id="クラスタとリソース">クラスタとリソース</h2>
<p>Kubernetes は、コンテナ化されたアプリケーションのデプロイや運用を効果的に行うためのプラットフォーム。<br/>
複数(場合によってはひとつ)のコンピュータ(物理マシンや仮想マシン)上でコンテナを動かす。このコンピュータのことをノードといい、ノードの集合体のことをクラスタという。</p>
<p>Docker Desktop で Kubernetes を動かす場合<code>docker-desktop</code>という名前のクラスタが作られるが、このクラスタを構成するノードは物理マシンや仮想マシンではない。Kubernetes を動かすために必要な各種コンポーネントが Docker Desktop 上でコンテナとして作られ、それらがノードの役割を果たす。<br/>
そのため、ローカルマシン上にクラスタが存在するが、ローカルマシンそのものがノードというわけではない。</p>
<p>Kubernetes には「リソース」という概念があり、それを使ってクラスタの状態や動作を記述する。そうすることで Kubernetes は、記述された内容を実現・維持しようとする。<br/>
リソースには様々な種類があり、適切なリソースに対して適切な記述をすることで、クラスタを意図通りに動作させることができる。<br/>
具体的には、マニフェストファイルと呼ばれる定義ファイルにリソースの設定を記述し、それを Kubernetes に伝える。本記事では<code>kubectl apply</code>コマンドを使ってマニフェストファイルの内容を Kubernetes に伝える。</p>
<p>早速<code>manifestfile.yaml</code>という名前のマニフェストファイルを作ってみる。<br/>
内容の説明は必要に応じて後から行うので、今は読まなくてもよい。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> apps/v1
<span class="synIdentifier">kind</span><span class="synSpecial">:</span> Deployment
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> my-dep<span class="synComment"> # Deployment の名前</span>
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">selector</span><span class="synSpecial">:</span><span class="synComment"> # Deployment が管理する Pod をどのように選択するか定義する</span>
<span class="synIdentifier">matchLabels</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app<span class="synComment"> # app:node-app というラベルの Pod をこの Deploymentが 管理する</span>
<span class="synIdentifier">replicas</span><span class="synSpecial">:</span> <span class="synConstant">3</span><span class="synComment"> # 保ちたい Pod の数</span>
<span class="synIdentifier">template</span><span class="synSpecial">:</span><span class="synComment"> # 作成する Pod の情報を書いていく</span>
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">labels</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">containers</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> my-pod
<span class="synIdentifier">image</span><span class="synSpecial">:</span> sample:latest<span class="synComment"> # 事前に作成した container image</span>
<span class="synIdentifier">imagePullPolicy</span><span class="synSpecial">:</span> IfNotPresent
<span class="synIdentifier">ports</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">containerPort</span><span class="synSpecial">:</span> <span class="synConstant">3000</span>
<span class="synPreProc">---</span>
<span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> v1
<span class="synIdentifier">kind</span><span class="synSpecial">:</span> Service
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> my-ser<span class="synComment"> # Service の名前</span>
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">type</span><span class="synSpecial">:</span> NodePort
<span class="synIdentifier">ports</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> my-ser-port
<span class="synIdentifier">port</span><span class="synSpecial">:</span> <span class="synConstant">8099</span><span class="synComment"> # Service の port</span>
<span class="synIdentifier">targetPort</span><span class="synSpecial">:</span> <span class="synConstant">3000</span><span class="synComment"> # Pod の port</span>
<span class="synIdentifier">nodePort</span><span class="synSpecial">:</span> <span class="synConstant">32660</span><span class="synComment"> # ワーカーノードの port</span>
<span class="synIdentifier">selector</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app<span class="synComment"> # Service が転送を行う Pod を指定</span>
</pre>
<p>そして<code>$ kubectl apply -f マニフェストファイルのパス</code>を実行する。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl apply -f manifestfile.yaml
deployment.apps/my-dep created
service/my-ser created
</pre>
<p>これで<code>sample</code>から作られたコンテナが Kubernetes 上で動いているはずなので、curl で動作確認してみる。なぜポート番号が<code>32660</code>なのかは後述するので気にしなくてよい。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>
Hello World
</pre>
<p>意図通りのレスポンスが返ってきた。<br/>
つまり、コンテナが動作しており、それに対してクラスタ外からアクセスできるようになっている。<br/>
そしてこれも後述するが、<code>sample</code>から作られたコンテナは 3 つ存在しており、クラスタはその数を維持しようとする。</p>
<p>本記事ではこれ以降、このクラスタがなぜそのような動作をしているのか、そしてそれを実現させる上で各リソースがどんな役割を果たしているのか、といったことを見ていく。そしてそれを通して Kubernetes の初歩を学んでいく。</p>
<p>一旦、クラスタの状態を元に戻しておく。反映させたマニフェストファイルの設定を取り消すためには、<code>$ kubectl delete -f マニフェストファイルのパス</code>を実行する。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl delete -f manifestfile.yaml
deployment.apps <span class="synConstant">"my-dep"</span> deleted
service <span class="synConstant">"my-ser"</span> deleted
</pre>
<h2 id="Pod-と-Deployment">Pod と Deployment</h2>
<p>Kubernetes は、Pod というリソースでコンテナを管理する。<br/>
ひとつの Pod で複数のコンテナを管理することもできるが、この記事ではひとつの Pod はひとつのコンテナのみを扱うことにする。</p>
<p>さらに、Pod を管理するための Deployment というリソースがある。Deployment を使うことでコンテナの管理が容易になる。</p>
<p>マニフェストファイルに Deployment について記述してみる。その場合、Deployment の設定のなかに Pod の設定を記述する。<br/>
<code>manifestfile.yaml</code>というファイルを作成し、そこに以下の内容を書いていく。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> apps/v1
<span class="synIdentifier">kind</span><span class="synSpecial">:</span> Deployment
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> my-dep<span class="synComment"> # Deployment の名前</span>
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">selector</span><span class="synSpecial">:</span><span class="synComment"> # Deployment が管理する Pod をどのように選択するか定義する</span>
<span class="synIdentifier">matchLabels</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app<span class="synComment"> # app:node-app というラベルの Pod をこの Deploymentが 管理する</span>
<span class="synIdentifier">replicas</span><span class="synSpecial">:</span> <span class="synConstant">3</span><span class="synComment"> # 保ちたい Pod の数</span>
<span class="synIdentifier">template</span><span class="synSpecial">:</span><span class="synComment"> # 作成する Pod の情報を書いていく</span>
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">labels</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">containers</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> my-pod
<span class="synIdentifier">image</span><span class="synSpecial">:</span> sample:latest<span class="synComment"> # 事前に作成した container image</span>
<span class="synIdentifier">imagePullPolicy</span><span class="synSpecial">:</span> IfNotPresent
<span class="synIdentifier">ports</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">containerPort</span><span class="synSpecial">:</span> <span class="synConstant">3000</span>
</pre>
<p>このマニフェストファイルでは以下の設定を行っている。</p>
<ul>
<li><code>my-dep</code>という名前の Deployment を作る
<ul>
<li>この Deployment は<code>app:node-app</code>というラベルの Pod を管理する</li>
<li>Pod を 3 つ作りそれを維持する</li>
</ul>
</li>
<li><code>my-pod</code>という名前の Pod を作る
<ul>
<li>この Pod のラベルは<code>app:node-app</code></li>
<li>コンテナイメージとして<code>sample:latest</code>を使う</li>
<li><code>imagePullPolicy: IfNotPresent</code>は、該当するコンテナイメージがないか、まずローカルを探すようにするための設定</li>
<li>コンテナは<code>3000</code>ポートで通信を受け付ける
<ul>
<li>冒頭で示したように<code>sample</code>は<code>3000</code>ポートで通信を受け付けるので、それに合わせている</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><code>apply</code>を実行して上記の設定をクラスタに反映させる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl apply -f manifestfile.yaml
deployment.apps/my-dep created
</pre>
<p><code>$ kubectl get deployments</code>でクラスタ内の Deployment 一覧を見れるので、確認してみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
my-dep <span class="synConstant">3</span>/<span class="synConstant">3</span> <span class="synConstant">3</span> <span class="synConstant">3</span> 34s
</pre>
<p><code>my-dep</code>が作られている。</p>
<p>同じ要領でクラスタ内の Pod 一覧も見れる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-dep-765c757fd8-4gxdp <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 42s
my-dep-765c757fd8-8p7m9 <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 42s
my-dep-765c757fd8-nxsck <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 42s
</pre>
<p>Pod が 3 つ作られている。</p>
<p><code>$ kubectl describe pod Pod名</code>で Pod の詳細情報を得られる。<br/>
多くの情報が表示されるので一部を抜粋して載せるが、<code>sample:latest</code>から作られた<code>my-pod</code>が<code>3000</code>ポートを開けて動いていることが分かる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl describe pod my-dep-765c757fd8-4gxdp
(省略)
Containers:
my-pod:
Container ID: docker://671805cbdd2a40895c135fd6e0d8d5438351d96fc2f1c0ea5e8f973610a4872a
Image: sample:latest
Image ID: docker://sha256:3ce5b6edbe20936ef2b9d495e107555f584760e1960d2257a4452ee4100230a1
Port: <span class="synConstant">3000</span>/TCP
Host Port: <span class="synConstant">0</span>/TCP
State: Running
(省略)
</pre>
<h2 id="Pod-の数は維持される">Pod の数は維持される</h2>
<p>先程「Pod を 3 つ作りそれを維持する」と書いたが、Deployment は指定された数の Pod を維持しようとする。</p>
<p>試しに Pod をひとつ削除してみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl delete pod my-dep-765c757fd8-4gxdp
pod <span class="synConstant">"my-dep-765c757fd8-4gxdp"</span> deleted
</pre>
<p>そのあとに<code>$ kubectl get pods</code>を実行すると以下の結果になる。</p>
<pre class="code" data-lang="" data-unlink>$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-dep-765c757fd8-8p7m9 1/1 Running 0 14m
my-dep-765c757fd8-cr46q 1/1 Running 0 4s
my-dep-765c757fd8-nxsck 1/1 Running 0 14m</pre>
<p><code>my-dep-765c757fd8-4gxdp</code>は確かに削除されたが、その代わりに<code>my-dep-765c757fd8-cr46q</code>が新しく作られ、Pod の数は 3 つに保たれている。</p>
<p>では、何らかの理由で数を変えたいときはどうすればよいのか。<br/>
マニフェストファイルの<code>spec.replicas</code>の記述を変え、再度 apply すればいい。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synType">--- a/manifestfile.yaml</span>
<span class="synType">+++ b/manifestfile.yaml</span>
<span class="synStatement">@@ -6,7 +6,7 @@</span><span class="synPreProc"> spec:</span>
selector: # Deployment が管理する Pod をどのように選択するか定義する
matchLabels:
app: node-app # app:node-app というラベルの Pod をこの Deploymentが 管理する
<span class="synSpecial">- replicas: 3 # 保ちたい Pod の数</span>
<span class="synIdentifier">+ replicas: 1 # 保ちたい Pod の数</span>
template: # 作成する Pod の情報を書いていく
metadata:
labels:
</pre>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl apply -f manifestfile.yaml
deployment.apps/my-dep configured
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-dep-765c757fd8-8p7m9 <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 17m
</pre>
<p>1 つになっている。</p>
<p><code>spec.replicas</code>を<code>3</code>に戻して apply すればまた 3 つになる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl apply -f manifestfile.yaml
deployment.apps/my-dep configured
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-dep-765c757fd8-68m8d <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 2s
my-dep-765c757fd8-8p7m9 <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 20m
my-dep-765c757fd8-ss7gm <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 2s
</pre>
<h2 id="Pod-の-IP-アドレス">Pod の IP アドレス</h2>
<p>Pod にはそれぞれ IP アドレスが振られている。<br/>
先程紹介した<code>$ kubectl describe pod</code>でもその Pod の IP アドレスを見れるが、<code>$ kubectl get pods -o wide</code>を使うと各 Pod の IP アドレスを一覧で見れる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
my-dep-765c757fd8-68m8d <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 101s <span class="synConstant">10.1</span>.<span class="synConstant">0.152</span> docker-desktop <span class="synStatement"><</span>none<span class="synStatement">></span> <span class="synStatement"><</span>none<span class="synStatement">></span>
my-dep-765c757fd8-8p7m9 <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 22m <span class="synConstant">10.1</span>.<span class="synConstant">0.149</span> docker-desktop <span class="synStatement"><</span>none<span class="synStatement">></span> <span class="synStatement"><</span>none<span class="synStatement">></span>
my-dep-765c757fd8-ss7gm <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 101s <span class="synConstant">10.1</span>.<span class="synConstant">0.151</span> docker-desktop <span class="synStatement"><</span>none<span class="synStatement">></span> <span class="synStatement"><</span>none<span class="synStatement">></span>
</pre>
<p>だがこの IP アドレスはクラスタ内部で通信するためのものであり、クラスタ外からこの IP アドレスを使って Pod と通信することはできない。<br/>
<code>$ curl 10.1.0.152:3000</code>を実行してもレスポンスは得られない。</p>
<p>クラスタ内部では通信できることを確認するため、Pod から Pod にリクエストを送ってみる。<br/>
<code>kubectl exec -it Podの名前 -- 実行したいコマンド</code>でコンテナ内でコマンドを実行できるので、それを使う。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl <span class="synStatement">exec</span> -it my-dep-765c757fd8-8p7m9 -- curl <span class="synConstant">10.1</span>.<span class="synConstant">0.152</span>:<span class="synConstant">3000</span>
Hello World
</pre>
<p><code>my-dep-765c757fd8-8p7m9</code>から<code>my-dep-765c757fd8-68m8d</code>(IP アドレス<code>10.1.0.152:3000</code>)にリクエストを送り、レスポンスを得られた。</p>
<p>しかし、いくらクラスタ内部で通信できたところで、外部からアクセスできないのでは、ウェブアプリケーションとしての実用性はない。<br/>
Service というリソースを使うことで、外部からアクセスできるようになる。</p>
<h2 id="Service">Service</h2>
<p>Service は通信に関する様々な役割を担う。</p>
<p>Deployment のときと同様、マニフェストファイルに Service を記述する。<br/>
リソース毎にマニフェストファイルを用意することもできるが、この記事では全てのリソースについてひとつのマニフェストファイル(<code>manifestfile.yaml</code>)に書くことにする。</p>
<p>複数のリソースをひとつのマニフェストファイルに書くときは<code>---</code>で区切る必要があるので、その下に Service を書いていく。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># 既に説明した Deployment の設定がここに書かれてある</span>
<span class="synPreProc">---</span>
<span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> v1
<span class="synIdentifier">kind</span><span class="synSpecial">:</span> Service
<span class="synIdentifier">metadata</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> my-ser<span class="synComment"> # Service の名前</span>
<span class="synIdentifier">spec</span><span class="synSpecial">:</span>
<span class="synIdentifier">type</span><span class="synSpecial">:</span> NodePort
<span class="synIdentifier">ports</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> my-ser-port
<span class="synIdentifier">port</span><span class="synSpecial">:</span> <span class="synConstant">8099</span><span class="synComment"> # Service の port</span>
<span class="synIdentifier">targetPort</span><span class="synSpecial">:</span> <span class="synConstant">3000</span><span class="synComment"> # Pod の port</span>
<span class="synIdentifier">nodePort</span><span class="synSpecial">:</span> <span class="synConstant">32660</span><span class="synComment"> # ワーカーノードの port</span>
<span class="synIdentifier">selector</span><span class="synSpecial">:</span>
<span class="synIdentifier">app</span><span class="synSpecial">:</span> node-app<span class="synComment"> # Service が転送を行う Pod を指定</span>
</pre>
<p><code>spec.type</code>として<code>NodePort</code>を設定した。<code>spec.type</code>には他の設定もあるが、この記事では<code>NodePort</code>である前提で話を進めていく。</p>
<p>apply して<code>my-ser</code>が作られていることを確認する。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl apply -f manifestfile.yaml
deployment.apps/my-dep unchanged
service/my-ser created
$ kubectl get services my-ser
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-ser NodePort <span class="synConstant">10.96</span>.<span class="synConstant">104.45</span> <span class="synStatement"><</span>none<span class="synStatement">></span> <span class="synConstant">8099</span>:<span class="synConstant">32660</span>/TCP 3s
</pre>
<p>Service は自身にアクセスがあると、それを Pod に転送してくれる。<br/>
どの Pod に転送するのかは、<code>spec.selector</code>で指定する。今回は<code>app: node-app</code>というラベルの Pod に転送したいので、そのように書いた。<br/>
また、Pod のポート番号を<code>spec.ports[].targetPort</code>に書く必要があるので、これも Pod の設定に合わせて<code>3000</code>を書いている。</p>
<p>これで、Service にリクエストがあると<code>app: node-app</code>ラベルの Pod、つまり先程 Deployment で作った Pod に転送されるようになった。</p>
<p>この設定が意図した通りに行われているか、<code>$ kubectl get endpoints サービス名</code>で確認できる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get endpoints my-ser
NAME ENDPOINTS AGE
my-ser <span class="synConstant">10.1</span>.<span class="synConstant">0.149</span>:<span class="synConstant">3000</span>,<span class="synConstant">10.1</span>.<span class="synConstant">0.151</span>:<span class="synConstant">3000</span>,<span class="synConstant">10.1</span>.<span class="synConstant">0.152</span>:<span class="synConstant">3000</span> 3m15s
</pre>
<p><code>my-ser</code>は、自身にリクエストがあったときにこの<code>Endpoints</code>のいずれかに転送してくれる。<br/>
そしてこれらのエンドポイントは、既に見た Pod の IP アドレスに<code>spec.ports[].targetPort</code>で設定したポート番号を組み合わせたものと一致している。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink> $ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
my-dep-765c757fd8-68m8d <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 33m <span class="synConstant">10.1</span>.<span class="synConstant">0.152</span> docker-desktop <span class="synStatement"><</span>none<span class="synStatement">></span> <span class="synStatement"><</span>none<span class="synStatement">></span>
my-dep-765c757fd8-8p7m9 <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 53m <span class="synConstant">10.1</span>.<span class="synConstant">0.149</span> docker-desktop <span class="synStatement"><</span>none<span class="synStatement">></span> <span class="synStatement"><</span>none<span class="synStatement">></span>
my-dep-765c757fd8-ss7gm <span class="synConstant">1</span>/<span class="synConstant">1</span> Running <span class="synConstant">0</span> 33m <span class="synConstant">10.1</span>.<span class="synConstant">0.151</span> docker-desktop <span class="synStatement"><</span>none<span class="synStatement">></span> <span class="synStatement"><</span>none<span class="synStatement">></span>
</pre>
<p>Service にリクエストを送ると Pod に転送してくれることは分かったが、そもそも Service へのリクエストはどのように行えばよいのか。</p>
<p>Service が作られると自動的に IP アドレスが割り振られるので、それを使って Service と通信を行うことができる。<br/>
<code>kubectl get services サービス名</code>で表示される<code>CLUSTER-IP</code>が、その IP アドレスである。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl get services my-ser
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-ser NodePort <span class="synConstant">10.96</span>.<span class="synConstant">104.45</span> <span class="synStatement"><</span>none<span class="synStatement">></span> <span class="synConstant">8099</span>:<span class="synConstant">32660</span>/TCP 10m
</pre>
<p>そして<code>spec.ports[].port</code>で指定した値が Service のポート番号になる。</p>
<p>つまり今回の場合、<code>10.96.104.45:8099</code>で Service と通信できる。</p>
<p>Pod 間通信のときと同様、Pod から Service にリクエストを送ってみる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ kubectl <span class="synStatement">exec</span> -it my-dep-765c757fd8-8p7m9 -- curl <span class="synConstant">10.96</span>.<span class="synConstant">104.45</span>:<span class="synConstant">8099</span>
Hello World
</pre>
<p><code>Hello World</code>が返ってきた。<br/>
<code>my-dep-765c757fd8-8p7m9</code>という Pod から Service にリクエストを送り、それを受け取った Service がいずれかの Pod にリクエストを転送、Pod 内で動いているコンテナがレスポンスを返したため、このような結果になった。</p>
<h2 id="ノードへのアクセス">ノードへのアクセス</h2>
<p>わざわざ Pod 経由で Service にリクエストを送ったことで気付いている方もいるかもしれないが、Service にも、クラスタ外からアクセスすることはできない。<br/>
<code>spec.type</code>が<code>NodePort</code>の場合、ノードに対してリクエストを送ることで、外部からクラスタにアクセスできるようになる。</p>
<p><code>NodePort</code>のときは全てのワーカーノード(ノードのうち、実際にその上でコンテナが動いているノード。他にマスターノードがある。)のポートが開くので、そこに対してリクエストを送ればよい。<br/>
具体的には<code>spec.ports[].nodePort</code>で指定した値が開かれる。そのため今回は<code>32660</code>になる。</p>
<p>Docker Desktop の場合、Docker Desktop を動かしているローカルマシン上にノードがひとつ存在し、そのノードがマスターノードとワーカーノードも兼ねており、<code>ローカルマシンのIPアドレス:32660</code>で外部からアクセスできるようになる。</p>
<pre class="code lang-zsh" data-lang="zsh" data-unlink>$ curl localhost:<span class="synConstant">32660</span>
Hello World
</pre>
<p>まとめると、「ノード -> Service -> Pod」という順番にルーティングされていくことで、Pod のなかで動いているコンテナがリクエストを受け取ることができるのである。</p>
numb_86
『Cプログラミング入門以前 [第3版]』を読んだ
hatenablog://entry/820878482941630560
2023-06-14T20:57:15+09:00
2023-06-14T20:57:15+09:00 プログラミングを学び始める前に押さえておくべき基礎知識を学べる一冊。 コンピュータやプログラムが動く仕組みをちゃんと理解しなければいけないと感じており、その一環で読んだ。 tatsu-zine.com 著者が執筆した『基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版』がよかったので、本書も手に取った。 『基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版』では、コンピュータネットワークを学ぶためにはまずコンピュータの仕組みを理解する必要がある、という考えに基づき、序盤でコンピュータの説明が行われる。 本書も同様に「C でプログラムを書くにはまず、コンピ…
<p>プログラミングを学び始める前に押さえておくべき基礎知識を学べる一冊。<br/>
コンピュータやプログラムが動く仕組みをちゃんと理解しなければいけないと感じており、その一環で読んだ。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftatsu-zine.com%2Fbooks%2Fc-programming-nyumon-izen-3ed" title="Cプログラミング入門以前 [第3版]" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tatsu-zine.com/books/c-programming-nyumon-izen-3ed">tatsu-zine.com</a></cite></p>
<p>著者が執筆した<a href="https://numb86-tech.hatenablog.com/entry/2019/10/30/130038">『基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版』</a>がよかったので、本書も手に取った。<br/>
『基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版』では、コンピュータネットワークを学ぶためにはまずコンピュータの仕組みを理解する必要がある、という考えに基づき、序盤でコンピュータの説明が行われる。<br/>
本書も同様に「C でプログラムを書くにはまず、コンピュータの仕組みを理解していたほうがいい」というコンセプトで、より初心者向けに噛み砕いて説明している。<br/>
プログラミング自体が初めてという読者を想定しているようで、かなり丁寧な説明になっている。</p>
<p>『基礎からわかるTCP/IP ネットワークコンピューティング入門 第3版』はコンピュータの説明をしたあとにネットワークについて学んでいくが、本書では C そのものの話はそれほど多くない。<br/>
本書の中で著者も述べているが、本書だけ読んでも C を使ってプログラミングできるようにはならない。<br/>
だが間違いなく、本書の内容を理解していたほうがその後のプログラミング学習がスムーズになると思う。<br/>
文字列なども内部的には全て数値で表現されていること、変数を宣言するとメモリに変数の領域が割り当てられること、データそのものをコピーするのではなくデータが入っているメモリ領域の先頭のアドレスを受け渡しすることで効率がよくなること、そういったことを知っておくことで、実際にプログラミングを行う際のルールや挙動を理解しやすくなる気がする。というか、そういう知識が欠けていると、例えば<code>malloc</code>の挙動などもよく分からないのではないだろうか。</p>
<p>図が多いのもよい。特に上記したメモリの割り当てなどは、図があることでかなり分かりやすくなる。</p>
numb_86
『なるほどUnixプロセス―Rubyで学ぶUnixの基礎』を読んだ
hatenablog://entry/820878482936100813
2023-05-27T21:10:00+09:00
2023-05-27T21:10:00+09:00 Unix プロセスとはどのようなもので、どのような特徴を持つのか、平易な文章と簡潔なコードを使って解説していく一冊。 tatsu-zine.com プロセスID、プロセスの親子関係、標準ストリームといった、プロセスに関する基本的な概念について説明していきながら、プロセスに対する理解を深めていく。 あくまでも入門的な内容だから、ということもあるだろうけど、とにかく読みやすい。 各章が短いので少しずつ読み進めていくことができるし、本書全体も 100 ページ超なので、気付いたら読み終えていた。 実際にコードを実行して確認できるのもいい。 forkで子プロセスを作ってそのpidやppidを確認したり、…
<p>Unix プロセスとはどのようなもので、どのような特徴を持つのか、平易な文章と簡潔なコードを使って解説していく一冊。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftatsu-zine.com%2Fbooks%2Fnaruhounix" title="なるほどUnixプロセス ― Rubyで学ぶUnixの基礎" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tatsu-zine.com/books/naruhounix">tatsu-zine.com</a></cite></p>
<p>プロセスID、プロセスの親子関係、標準ストリームといった、プロセスに関する基本的な概念について説明していきながら、プロセスに対する理解を深めていく。</p>
<p>あくまでも入門的な内容だから、ということもあるだろうけど、とにかく読みやすい。<br/>
各章が短いので少しずつ読み進めていくことができるし、本書全体も 100 ページ超なので、気付いたら読み終えていた。</p>
<p>実際にコードを実行して確認できるのもいい。<br/>
<code>fork</code>で子プロセスを作ってその<code>pid</code>や<code>ppid</code>を確認したり、<code>fork</code>が値を 2 回返すことを確認したり、シグナルを送ってプロセスを終了させることができることを確認したり、といったことを繰り返していくことで、理解が深まっていく。<br/>
環境構築が簡単なのもよい。Unix 系の OS で、ターミナルと Ruby さえあればいい。Mac だったら、何もしなくても揃っているのではないだろうか。</p>
<p>ただ残念ながら Spyglass を動かすことはできなかった。<br/>
Spyglass とは本書のために書かれたウェブサーバで、付録としてついてくる。ウェブサーバとして機能させるためにプロセスがどのように動いているのかを知り、理解を深めることを目的としている。<br/>
ネット上の情報も参考にしながらビルドを試みたが、上手くいかなかった。原書が出版されたのはかなり昔のようなので、仕方ないかもしれないが。</p>
<p>本文中に出てくるコードが簡潔なのも素晴らしい。<br/>
主題とは無関係な記述が多くサンプルコードが冗長になってしまっている事象を見かけるが、本書ではそのようなことはない。<br/>
どのコードもシンプルさを保っており、ほとんどのサンプルコードが 10 行以内に収まっている。<br/>
そのため、Ruby を全く知らなくても、何をしているのか概ね理解できる気がする。今なら ChatGPT に聞いてもいいわけだし。</p>
<p>そしてもちろん、本書の説明そのものが分かりやすい。<br/>
私のような初学者でも理解できるように、とても丁寧に説明されている。著者が文章が上手いというのもあるだろうし、それが自然に翻訳されているというのもある。訳書にありがちな読みづらさは全くなかった。<br/>
専門書を母国語で読めるのは本当にありがたい。いろんなニュースを見ていると、訳書に限らず専門書の出版は難しくなっているようだけど、何とか頑張って欲しい。</p>
<p>以下は訳者の一人が執筆された記事なのだが、この記事自体もかなり分かりやすい。<br/>
本書の一部を抜粋したような内容なので、これを読むと本書の雰囲気を掴めると思う。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmagazine.rubyist.net%2Farticles%2F0060%2F0060-NaruhodoUnixTip.html" title="『なるほどUnixプロセス』を読む前にちょっとだけナルホドとなる記事" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://magazine.rubyist.net/articles/0060/0060-NaruhodoUnixTip.html">magazine.rubyist.net</a></cite></p>
numb_86
『Software Design 2023年4月号』の「x86やArmって何? 一度は学んでおきたいCPUのしくみ」を読んだ
hatenablog://entry/820878482935568134
2023-05-25T22:50:01+09:00
2023-05-25T22:54:56+09:00 CPU について学ぶ初歩的な教材としてよさそうと思い、手に取った。 期待した通り、平易な内容で程よくコンパクトにまとまっており、読みやすかった。 gihyo.jp CPU はレジスタや ALU から構成されること、CPU は0と1で構成される機械語しか理解できないため高水準言語も最終的には機械語に変換されること、などの初歩的な内容から始まり、命令セットアーキテクチャやマイクロアーキテクチャの話、CPU の性能を引き出すための工夫、などについても扱っている。 既に一定以上の知識を持っている人にとっては物足りない内容かもしれないが、少なくとも自分は読んでよかった。 なんとなくぼんやり理解していたこ…
<p>CPU について学ぶ初歩的な教材としてよさそうと思い、手に取った。<br/>
期待した通り、平易な内容で程よくコンパクトにまとまっており、読みやすかった。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgihyo.jp%2Fmagazine%2FSD%2Farchive%2F2023%2F202304" title="Software Design 2023年4月号" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://gihyo.jp/magazine/SD/archive/2023/202304">gihyo.jp</a></cite></p>
<p>CPU はレジスタや ALU から構成されること、CPU は<code>0</code>と<code>1</code>で構成される機械語しか理解できないため高水準言語も最終的には機械語に変換されること、などの初歩的な内容から始まり、命令セットアーキテクチャやマイクロアーキテクチャの話、CPU の性能を引き出すための工夫、などについても扱っている。</p>
<p>既に一定以上の知識を持っている人にとっては物足りない内容かもしれないが、少なくとも自分は読んでよかった。<br/>
なんとなくぼんやり理解していたことについて確認したり、曖昧だった部分を整理したりすることが出来た。<br/>
例えば、x86 や Arm が CPU の種類を表現する何かだということは分かっていたが、それが「命令セットアーキテクチャ」というもののことだと初めて知ったし、 Apple の M シリーズは Arm であることや、RISC-V という新興の命令セットアーキテクチャが注目されていることなどを知れた。CISC と RISC という分類も初めて知った。</p>
<p>ハードウェアや低レイヤーの書籍は行間が広いというか、「これくらい知ってるだろ、知っていてくれ」と説明が省略されていることが多い印象があるが、本特集はそういう部分がほとんどなくて読みやすかった。</p>
<p>コンピュータや CPU の仕組みに関心があるけど敷居が高くて何から学べばいいか分からない、という人におすすめ。</p>
numb_86
『ゾンビスクラムサバイバルガイド 健全なスクラムへの道』を読んだ
hatenablog://entry/4207575160646029860
2023-05-05T20:01:12+09:00
2023-05-05T20:01:12+09:00 スクラムのように見えるがスクラムではない、機能していないスクラムを「ゾンビスクラム」と名付け、なぜそれが発生するか、いかにそこから回復すべきかを説いた一冊。 ゾンビスクラムという、上手くいっていない状態を題材にすることで、本来はどうあるべきなのか、どのような状態を目指すべきなのかが、自然と理解できるようになっている。 www.hanmoto.com スクラムガイドの内容がシンプルすぎることが、スクラムフレームワークの実践の難しさの一つになっていると思っている。 PBI には何を書けばいいのか、リファインメントはどのように行うのか、各種スクラムイベントはどのように進めればいいのか、具体的な方法は…
<p>スクラムのように見えるがスクラムではない、機能していないスクラムを「ゾンビスクラム」と名付け、なぜそれが発生するか、いかにそこから回復すべきかを説いた一冊。<br/>
ゾンビスクラムという、上手くいっていない状態を題材にすることで、本来はどうあるべきなのか、どのような状態を目指すべきなのかが、自然と理解できるようになっている。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.hanmoto.com%2Fbd%2Fisbn%2F9784621307397" title="ゾンビスクラムサバイバルガイド 木村 卓央(翻訳) - 丸善出版" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.hanmoto.com/bd/isbn/9784621307397">www.hanmoto.com</a></cite></p>
<p>スクラムガイドの内容がシンプルすぎることが、スクラムフレームワークの実践の難しさの一つになっていると思っている。<br/>
PBI には何を書けばいいのか、リファインメントはどのように行うのか、各種スクラムイベントはどのように進めればいいのか、具体的な方法はほとんど記述されていない。<br/>
組織や事業によって状況は異なるのだから、具体的なことが書かれていないのは当然だとは思う。<br/>
それに、何か具体的な方法論を書いてしまうと、どれだけそれを「一例である」と断ったところで、それが独り歩きしてしまうのは目に見えている。そしてそれさえやっていればいいと誤解されたり、逆にそれ以外のやり方は許されないと解釈する人たちが現れたりしてしまう。<br/>
だから、スクラムガイドがシンプルなものになっていること自体は、妥当なことだとは思う。</p>
<p>しかしシンプルにすると今度は、別の問題を生む。<br/>
具体的にどう実践していいのか分からないし、都合の良いように解釈できてしまう。<br/>
結果的に、スクラムガイドに書かれている「プロダクトバックログ」や「スクラムイベント」、「スプリント」といったものを形式的に導入しただけで挫折してしまう。あるいは逆に、形式を満たしただけで満足してしまう。<br/>
また、スクラムガイドが具体的な指示をしていないことをいいことに「これが俺たちのスクラムだ!」と都合の良い解釈をして、スクラムもアジャイルも関係ない何でもありの状態になってしまうこともある。</p>
<p>ちょっと前にバズっていた以下の記事にもあるように、スクラム風だが中身は全く違う、スクラムっぽいだけの何かになってしまうというのは、本当によくある事象なのだと思う。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fshin_semiya%2Farticles%2F7e2653da51da0c" title="「こうしてスクラムが終わってしまう」前にすべきこと" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://zenn.dev/shin_semiya/articles/7e2653da51da0c">zenn.dev</a></cite></p>
<p>本書ではそのような状態をゾンビスクラムと名付けている。<br/>
遠くから見るとスクラムなのだが、近くで見ると全く違う。脳はあまり動いていないし、何より心臓の鼓動がない。</p>
<p>ゾンビスクラムにありがちな症例を紹介していくと同時に、本来はどうあるべきかも語られていくが、これが本書の核だと思っている。<br/>
スクラムガイドだけだと理解しづらかったり見失ってしまいがちだったりすることについて、具体例を交えながら分かりやすく説明している。<br/>
「○○であるべき」という説明だけでなく、「××になっていてはいけない」「××は危険な徴候」という説明もあることで、より具体的にイメージできるようになっている。</p>
<p>タイトルに「サバイバルガイド」とあるように、ゾンビスクラムから回復するための方策も書かれてはいる。<br/>
しかしテクニカルなものはほとんどなく、透明性やより有益な対話を生み出し、チームの学習を促すためのものが大半を占める。<br/>
本書にも書かれているが、組織によって状況は異なるのだから、よその組織の事例をただ単にコピーしても上手くいかない。<br/>
そのため、出来合いの方法論を引っ張ってくるのではなく、学習できる組織になることが大切である。</p>
<p>本書が主張していることはシンプルで、ステークホルダーにとって価値のあるものを速く届けよう、ということに尽きる。<br/>
いくらたくさんのアウトプットを生み出しても、それらが価値を生み出していなければ意味がない。<br/>
しかしプロダクト開発は複雑な仕事であるため、何に価値があるのか事前に知ることは不可能。しかも、ニーズや市場環境は常に変化していくため、「何に価値があるのか」は常に変化していく。だからこそ、実際に動くプロダクト、スクラムの言葉でいえばインクリメントを、短いサイクルで提供しステークホルダーからフィードバックを受けることが大切になる。プロダクト開発という複雑な仕事においては、事前の予測は不可能であり、失敗は避けることはできない。そのため、とにかく速く小さく失敗し、そこから学習していくことが求められる。速く届けることはビジネス上のアドバンテージにもなる。<br/>
そしてそれは簡単にできることではない。チーム内外に様々な阻害要因がある。そのためチームは、仕事のやり方や環境を継続的に改善していく必要があるし、そのためにはチームは自分たちのことを自分たちで決められる状態になっていなければならない。</p>
<p>本書では上記の内容を「ステークホルダーが求めるものを作る」「速く出荷する」「継続的に改善する」「自己組織化する」の 4 つに分類し、それぞれの分野でゾンビスクラムではどのような症状が出るのか、そしてなぜそうなってしまうのか、説明していく。<br/>
そのどれもが、ありがちというか、耳が痛いものばかりだった。読んでいて何度も、「やめろ、もうやめてくれ」という気持ちになった。<br/>
最後に、耳が痛かった本書の指摘をいくつか載せておく。</p>
<ul>
<li><blockquote><p>ゾンビスクラムに苦しむチームは、スプリントの最後に価値のあるものを届けるのに苦労している。多くの場合、動くインクリメントすらない。(中略)この症状はスプリントレビューで明らかになる。ステークホルダーは作成されたプロダクトを自分で直接さわり、検証する機会がない。チームはプロジェクターの電源を入れて派手なプレゼンテーションをしたり、スクリーンショットを見せたり、スプリントバックログにあった内容をただ話すのみである。プロダクトが検査されたとしても、「次のスプリントで終わらせます」とか「おっと、まだ動かない」などのコメントが付けられるか、非常に技術的な話かのどちらかだ。 p20-p21</p></blockquote></li>
<li><blockquote><p>腕がもげても文句を言わないゾンビのように、ゾンビスクラムチームは、スプリントが成功しようが失敗しようが何の反応も示さない。他のチームが毒を吐いたり喜んだりしていても、彼らは感情もなく諦めたように虚ろな目でじっと眺めているだけだ。チームの士気は低い。スプリントバックログアイテムは、何の疑問もなく次のスプリントに持ち越される。 p22</p></blockquote></li>
<li><blockquote><p>目的や戦略がなければ、スクラムチームは何でもありのイケイケ開発に行き着いてしまう。そこでは、すべての作業が優先度「高(または低)」になる。その結果、肥大し続ける巨大なプロダクトバックログを抱えてしまうことになる。 p54</p></blockquote></li>
<li><blockquote><p>スプリントからスプリントへとアイテムを持ち越し続けていくと、問題はさらに大きくなり、スプリントは出荷はおろか、実際には何も完成できない意味のないタイムボックスであると感じるようになってしまう。 p117</p></blockquote></li>
<li><blockquote><p>開発チームが獲得すべき重要なスキルの 1 つが、大きなアイテムを小さなアイテムに分割するスキルと創造性だ。開発チームは、コードを書くことから作業を始めるのではなく「多くのことを学び、届けるものの価値を高めるために、構築してデプロイできる最小のものは何か」と問い、挑戦し続けることを学ぶべきである。 p118</p></blockquote></li>
<li><blockquote><p>スクラムチームが学ぶべき重要なスキルに、何を改善すべきかを具体的にする方法、改善を小さく分割する方法がある。大きなプロダクトバックログアイテムを小さなアイテムに分割して完成しやすくするように、大きな改善を小さく分割すると改善が成功しやすくなる。ゾンビスクラムに苦しむチームは、「プロダクトオーナーはもっと大きな権限を持つべきだ」というような、やる気をなくすような大がかりな改善で身動きが取れなくなったり、どこから始めればよいのかわからない漠然とした改善に途方に暮れる傾向がある。 p170</p></blockquote></li>
<li><blockquote><p>従来の管理では、チームや部署、従業員の仕事が、組織で定められた計画や目的、戦略に沿っていることを確実にすることが、管理職の主な仕事とされてきた。(中略)自己管理チームは、作業を調整し、チーム内およびチーム間の自己組織化を促進するために、これとは異なる仕組みを使う。専任の役割(管理職)や標準化された構造(階層や方針)の代わりに、人を奮い立たせるゴールと惹きつける目的によって自己調整を行う。p229-p230</p></blockquote></li>
<li><blockquote><p>成功するために他の人に求めることをはっきりと言葉にするのは簡単ではない。また、そのような要求を受けたときに明確な対応をするのも簡単ではない。曖昧なコミュニケーションは不満や非難に繋がりやすい。それでは上手くいかない。自己管理するチームが成功するためには他の人からの協力が多く必要だからだ。 p251</p></blockquote></li>
</ul>
numb_86
『熊とワルツを - リスクを愉しむプロジェクト管理』を読んだ
hatenablog://entry/4207112889963294154
2023-02-19T15:18:25+09:00
2023-02-19T15:18:25+09:00 「リスク」をどのように捉え、どのように向き合っていくべきなのか説いた一冊。 用語や概念の整理をしつつ、具体的にどのように取り組むべきかを論じていく。 2003 年頃に出版されたということもあってか、ソフトウェアの受託開発を念頭に置いた説明が多いが、基本的な考え方はそれ以外のプロジェクトにも適用できるはず。 bookplus.nikkei.com リスクを取らずに済むのであれば、そうすればよい。わざわざ危ない橋を渡る必要はない。 だがリスクと利益は切っても切れない関係にあり、成功を掴むためにはリスクを避けて通ることはできない。 著者らは、「リスクのないプロジェクトに手を出してはいけない」とまで言…
<p>「リスク」をどのように捉え、どのように向き合っていくべきなのか説いた一冊。<br/>
用語や概念の整理をしつつ、具体的にどのように取り組むべきかを論じていく。<br/>
2003 年頃に出版されたということもあってか、ソフトウェアの受託開発を念頭に置いた説明が多いが、基本的な考え方はそれ以外のプロジェクトにも適用できるはず。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbookplus.nikkei.com%2Fatcl%2Fcatalog%2F03%2FP81860%2F" title="熊とワルツを" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://bookplus.nikkei.com/atcl/catalog/03/P81860/">bookplus.nikkei.com</a></cite></p>
<p>リスクを取らずに済むのであれば、そうすればよい。わざわざ危ない橋を渡る必要はない。<br/>
だがリスクと利益は切っても切れない関係にあり、成功を掴むためにはリスクを避けて通ることはできない。<br/>
著者らは、「リスクのないプロジェクトに手を出してはいけない」とまで言う。<br/>
しかし同時に、リスクを無視するのも愚かな行為であると主張する。</p>
<p>不確定性を数量化し可視化すること、過去のデータを活用すること、あたりがリスク管理の中心的な考え方なのかなと読んでいて思った。</p>
<p>未来は不確実であり、誰にも分かりはしない。「このシステムはいつになったら完成するんだ、正確な納品日を教えろ」なんて聞かれても、分かるわけない。<br/>
だが、分からないなりに分かることだってある。「どんなに早くても来年の 3 月までに完成する可能性はゼロだろう。だがさすがに 12 月には完成しているはず」。<br/>
こうすると、依然として未来は不確実ではあるが、不確定性の範囲を示すことができる。</p>
<p>また、未来は分からないが過去に何が起こったのかは分かる。そして過去の実績は未来を予測するための貴重なデータになる。これまでのプロジェクトはどのようなリスクを抱えていて、それはどの程度実現し、プロジェクトの結果はどうだったのか。<br/>
それを記録しておくことで、より詳細に不確定性を数量化できる。</p>
<p>そして不確定性を可視化することで、どの程度の不確定性があるのかを正確に表現し伝えることができる。<br/>
本書自身がそれを実践・証明しており、図を使って説明してくれることで内容がすんなり入ってくるし、齟齬も生まれない。<br/>
視覚的に表現することで分かりやすくなるんだということを実感すると同時に、同じデータであってもそれをどう表現するかで得られる示唆は変わるんだなということも感じた。<br/>
本書では不確定性を漸増図と累積図で表現している。どちらも同じデータを違う形で表現しているだけだが、どちらを使うかで得られる示唆が変わってくる。知りたい内容に応じて適切な可視化方法を選ぶ必要がある。</p>
<p>可視化した不確定性のなかから特定の日付を選んでコミットメントするのは望ましくなく、可視化された不確定性そのものをスケジュールの約束にすべき、というのも尤もだなと思った。<br/>
確かにそれが望ましいとは思う。</p>
<p>とはいえ、そう簡単にはいかない。<br/>
本書を読んで一番強く感じたのが、リスク管理を行うためにはそのための土壌が必要だということ。著者らによれば「リスク管理は大人のプロジェクト管理であり成熟の証」とのことだが、まさに成熟した組織でないと不可能な取り組みだと思う。<br/>
本書でも何度も言及されているが、リスクを直視できない組織は多い。</p>
<p>リスク管理を行うためにはまず、不確定性やリスクの存在を認めることが重要なのだが、それがそもそも難しい。<br/>
不確定性やリスクの存在は「怠け」や「弱気」の印である、と見做す価値観や文化は確かに存在する。魅力のない結果ではなく魅力のない予測を罰する文化、失敗することは許されるが「失敗するかも」と口に出すことは許されない文化。<br/>
そういう文化においては、致命的なリスクからは目を背けるようになる。そのようなことについて考えるのが恐ろしいので、存在しないものとして扱ってしまう。そして、約束を守ることより、とにかく大きな約束をすることが大切になってしまう。<br/>
そのような環境では、リスク管理を実践するのは不可能だろう。</p>
<p>結局は文化や組織風土の話であり、「リスク管理」に限らず何事もそうだと思う。文化は組織の土台であり、それが腐っていては、その上にどんな立派な制度や仕組みを載せたところで上手くいかない。</p>
numb_86
MySQL の SQL Mode について
hatenablog://entry/4207112889962503845
2023-02-12T16:03:04+09:00
2023-02-12T16:03:04+09:00 MySQL には SQL Mode という設定があり、この内容によって、許容される構文やデータの妥当性チェックのルールが変化する。 この記事では SQL Mode の確認方法や設定方法の他、設定内容によって挙動が変化する例を見ていく。 動作確認は MySQL のバージョン8.0.28で行った。 環境構築 まずは動作確認用の MySQL コンテナを用意し起動する。 % docker run --name test_db -dit -e MYSQL_ROOT_PASSWORD=password mysql:8 以下のコマンドを入力するとパスワードを求められるので、先程MYSQL_ROOT_PASS…
<p>MySQL には SQL Mode という設定があり、この内容によって、許容される構文やデータの妥当性チェックのルールが変化する。<br/>
この記事では SQL Mode の確認方法や設定方法の他、設定内容によって挙動が変化する例を見ていく。<br/>
動作確認は MySQL のバージョン<code>8.0.28</code>で行った。</p>
<h2 id="環境構築">環境構築</h2>
<p>まずは動作確認用の MySQL コンテナを用意し起動する。</p>
<pre class="code" data-lang="" data-unlink>% docker run --name test_db -dit -e MYSQL_ROOT_PASSWORD=password mysql:8</pre>
<p>以下のコマンドを入力するとパスワードを求められるので、先程<code>MYSQL_ROOT_PASSWORD</code>として設定した<code>password</code>を入力する。</p>
<pre class="code" data-lang="" data-unlink>% docker exec -it test_db mysql -p</pre>
<h2 id="確認と設定">確認と設定</h2>
<p>現在の SQL Mode の設定内容は<code>sql_mode</code>というシステム変数に保持されている。<br/>
グローバルとセッションそれぞれ、<code>SELECT @@GLOBAL.sql_mode;</code>や<code>SELECT @@SESSION.sql_mode;</code>で確認できる。</p>
<pre class="code" data-lang="" data-unlink>mysql> SELECT @@GLOBAL.sql_mode;
+-----------------------------------------------------------------------------------------------------------------------+
| @@GLOBAL.sql_mode |
+-----------------------------------------------------------------------------------------------------------------------+
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+-----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> SELECT @@SESSION.sql_mode;
+-----------------------------------------------------------------------------------------------------------------------+
| @@SESSION.sql_mode |
+-----------------------------------------------------------------------------------------------------------------------+
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+-----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)</pre>
<p>デフォルトの設定内容はバージョンによって異なる。</p>
<p>設定を更新する方法はいくつかあるが、セッションの場合は例えば、<code>SET SESSION sql_mode = '設定したいモード';</code>で設定できる。<br/>
以下は、<code>STRICT_TRANS_TABLES</code>モードを有効にしている様子。</p>
<pre class="code" data-lang="" data-unlink>mysql> SET SESSION sql_mode = 'STRICT_TRANS_TABLES';
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> SELECT @@SESSION.sql_mode;
+---------------------+
| @@SESSION.sql_mode |
+---------------------+
| STRICT_TRANS_TABLES |
+---------------------+
1 row in set (0.00 sec)</pre>
<p>カンマ区切りで複数の SQL Mode を設定することもできる。</p>
<pre class="code" data-lang="" data-unlink>mysql> SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_DATE';
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> SELECT @@SESSION.sql_mode;
+----------------------------------+
| @@SESSION.sql_mode |
+----------------------------------+
| STRICT_TRANS_TABLES,NO_ZERO_DATE |
+----------------------------------+
1 row in set (0.00 sec)</pre>
<h2 id="SQL-Mode-によって挙動が変わる例">SQL Mode によって挙動が変わる例</h2>
<p>冒頭に書いた通り、 SQL Mode の設定内容によってクエリ実行時の挙動が変化する。<br/>
一例として、<code>IGNORE_SPACE</code>という SQL Mode の有無によって挙動がどのように変わるのか見てみる。</p>
<p>まずは動作確認用にデータを用意する。</p>
<pre class="code" data-lang="" data-unlink>mysql> CREATE DATABASE sample_db;
Query OK, 1 row affected (0.01 sec)
mysql> USE sample_db;
Database changed
mysql> CREATE TABLE users (id INT AUTO_INCREMENT, name TEXT NOT NULL, PRIMARY KEY (id));
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO users(name) VALUES('Alice');
Query OK, 1 row affected (0.02 sec)</pre>
<p><code>sample_db</code>というデータベースを作成し、そのなかに<code>users</code>というテーブルを作成、そしてそこに 1 件のレコードを作成した。</p>
<pre class="code" data-lang="" data-unlink>mysql> SELECT * FROM users;
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
+----+-------+
1 row in set (0.00 sec)</pre>
<p>まずは SQL Mode が何も設定されていない状態にしておく。</p>
<pre class="code" data-lang="" data-unlink>mysql> SET SESSION sql_mode = '';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@SESSION.sql_mode;
+--------------------+
| @@SESSION.sql_mode |
+--------------------+
| |
+--------------------+
1 row in set (0.00 sec)</pre>
<p>この状態でまず<code>SELECT COUNT( * ) FROM users;</code>を実行する。<br/>
そうすると、レコード数を取得できる。</p>
<pre class="code" data-lang="" data-unlink>mysql> SELECT COUNT( * ) FROM users;
+------------+
| COUNT( * ) |
+------------+
| 1 |
+------------+
1 row in set (0.00 sec)</pre>
<p>だが<code>SELECT COUNT ( * ) FROM users;</code>は、エラーになる。</p>
<pre class="code" data-lang="" data-unlink>mysql> SELECT COUNT ( * ) FROM users;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '* ) FROM users' at line 1</pre>
<p>これは、<code>COUNT</code>と<code>(</code>の間にスペースがあることで構文エラーになるためである。</p>
<p><code>IGNORE_SPACE</code>を有効にして再度<code>SELECT COUNT ( * ) FROM users;</code>を実行してみる。</p>
<pre class="code" data-lang="" data-unlink>mysql> SET SESSION sql_mode = 'IGNORE_SPACE';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@SESSION.sql_mode;
+--------------------+
| @@SESSION.sql_mode |
+--------------------+
| IGNORE_SPACE |
+--------------------+
1 row in set (0.00 sec)
mysql> SELECT COUNT ( * ) FROM users;
+-------------+
| COUNT ( * ) |
+-------------+
| 1 |
+-------------+
1 row in set (0.00 sec)</pre>
<p>今度はエラーにならない。<br/>
これは、<code>IGNORE_SPACE</code>によって関数名と<code>(</code>の間にスペースが入ることが許可されるようになったためである。</p>
<p>このように、同じクエリでも SQL Mode の設定内容によって挙動が変化することがある。</p>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.0/ja/sql-mode.html">MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.11 サーバー SQL モード</a></li>
<li><a href="https://songmu.jp/riji/entry/2015-07-08-kamipo-traditional.html">ルーク!MySQLではkamipo TRADITIONALを使え! | おそらくはそれさえも平凡な日々</a></li>
</ul>
numb_86
『UNIXという考え方―その設計思想と哲学』を読んだ
hatenablog://entry/4207112889952044579
2023-01-29T15:02:46+09:00
2023-01-29T15:02:46+09:00 UNIX やそのツールはどのような考えに基づいて作られているのか解説した本。 UNIX が開発されていくなかで培われていった文化や考え方について書かれている。 www.ohmsha.co.jp UNIX が具体的にどのように動いているのかではなく、 UNIX はなぜそのように動いているのか、ということが主題。 そのため、 UNIX に限らずソフトウェア開発全般に適用できるような内容になっている。ソフトウェアだけでなく「ものを作る」こと全般に応用できる内容も多いかもしれない。 私も、現時点では UNIX そのものに対する熱意や探究心はあまりないので、 UNIX について知るためではなく開発の参考…
<p>UNIX やそのツールはどのような考えに基づいて作られているのか解説した本。<br/>
UNIX が開発されていくなかで培われていった文化や考え方について書かれている。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ohmsha.co.jp%2Fbook%2F9784274064067%2F" title="UNIXという考え方 | Ohmsha" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.ohmsha.co.jp/book/9784274064067/">www.ohmsha.co.jp</a></cite></p>
<p>UNIX が具体的にどのように動いているのかではなく、 UNIX はなぜそのように動いているのか、ということが主題。<br/>
そのため、 UNIX に限らずソフトウェア開発全般に適用できるような内容になっている。ソフトウェアだけでなく「ものを作る」こと全般に応用できる内容も多いかもしれない。<br/>
私も、現時点では UNIX そのものに対する熱意や探究心はあまりないので、 UNIX について知るためではなく開発の参考になる考え方がないかと思って読んだ。</p>
<p>9 つの定理が紹介されているのだが、まず思ったのは、「言うは易く行うは難し」という感じの定理ばかりだなということ。<br/>
例えばシンプルに保て、小ささを維持しろ、というのは単純な定理だし、ほとんどの開発者が賛同するはず。<br/>
しかし実践するのが難しい。気が付けば余計なことをしているし、いつの間にかなぜか肥大化し複雑になっている。</p>
<p>目の前のタスクに追われているうちに定理を忘れがち、という側面もあるだろうが、単純な定理であっても実践するのは難しいという話なのかもしれない。<br/>
プログラムや関数は、たったひとつのやるべきことに専念するべきなのだろうが、その「やるべきこと」が何かを決めるのがまず難しい。<br/>
やりたいことを適切に抽象化して「やるべき」ことを導き出すのは、とてつもなく難しいことのように思える。</p>
<blockquote><p>一つのことをうまくやるようにプログラムを作れないのであれば、恐らく問題をまだ完全には理解していないのだろう</p></blockquote>
<p>と本書にも書かれているが、まさにそういう話だと思う。適切に設計するためにはまず、解きたい課題を正しく理解しないといけない。</p>
<p>アジャイル開発との共通性も印象に残った。<br/>
「できるだけ早く試作する」という定理は、何を作るべきか事前に判断するのは難しい、だからこそ実際に動くものを早く作って顧客からフィードバックをもらえ、試行錯誤を繰り返せ、という内容であり、アジャイル開発そのものだった。<br/>
そして本書の最後に、 UNIX の考え方とは変化し続ける世界で未来に向かっていくアプローチだ、という話が出てくるが、これも変化を受け入れるという意味でアジャイル開発の考え方と近いと思う。</p>
<p>それ以外だと、データを作るのはコンピュータではなく人間である、という話が面白くて印象に残った。<br/>
ワードプロセッサは自力では何も書けず、書くべき内容は人間の頭の中から生まれる、という話で、確かにという感じだった。<br/>
この話の少し前に書かれていた「コンピュータを使って積極的に処理の自動化を行うべきだ、処理に人間を介在させると人間の処理能力がボトルネックになってしまう、それではせっかくのコンピュータの能力を活かせない」という内容と合わせて考えると、コンピュータが得意なことはコンピュータに任せ、人間は人間にしか出来ないことに注力すべき、という話なのかもしれない。<br/>
近年はコンピュータが文章やイラストを自動生成してくれるが、あれもあくまでも人間がこれまで積み上げてきたデータを利用して生み出しているだけ、とも言えるかもしれない。<br/>
それでも構わない、むしろそのほうが望ましいというケースも当然あるだろうから、そういう分野ではコンピュータの利用が進むのだろうし、一方で人間でなければ生み出せないデータや人間が生み出すことに意味のあるデータも存在するはずなので、人間はそういったデータの生成に注力するようになっていくのだろう。</p>
numb_86
Web API で文字列を可逆圧縮する
hatenablog://entry/4207112889949300628
2023-01-22T17:12:46+09:00
2023-01-22T17:12:46+09:00 この記事では、 Web API で文字列の可逆圧縮を行う方法について書いていく。 任意の文字列を圧縮し、そして圧縮された文字列のリテラル表現から元の文字列を復元できることを目指す。 以前書いたように、 Node.js なら文字列の可逆圧縮は簡単に行える。 numb86-tech.hatenablog.com また、 JavaScript でデータの圧縮を行うためのライブラリも、探してみれば色々と見つかる。 だがこの記事では、ブラウザ環境でも動作するコードを、ライブラリに頼らずに実装していく。 完成したコードは成果物の節に載せてある。 この記事に出てくるコードの動作確認は以下の環境で行った。 D…
<p>この記事では、 Web API で文字列の可逆圧縮を行う方法について書いていく。<br/>
任意の文字列を圧縮し、そして圧縮された文字列のリテラル表現から元の文字列を復元できることを目指す。</p>
<p>以前書いたように、 Node.js なら文字列の可逆圧縮は簡単に行える。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2020%2F10%2F25%2F193353" title="テキストリソースを圧縮してウェブサイトのパフォーマンスを改善する - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://numb86-tech.hatenablog.com/entry/2020/10/25/193353">numb86-tech.hatenablog.com</a></cite></p>
<p>また、 JavaScript でデータの圧縮を行うためのライブラリも、探してみれば色々と見つかる。</p>
<p>だがこの記事では、ブラウザ環境でも動作するコードを、ライブラリに頼らずに実装していく。<br/>
完成したコードは<a href="#%E6%88%90%E6%9E%9C%E7%89%A9">成果物</a>の節に載せてある。</p>
<p>この記事に出てくるコードの動作確認は以下の環境で行った。</p>
<ul>
<li>Deno 1.29.1</li>
<li>TypeScript 4.9.4</li>
</ul>
<h2 id="Compression-Streams-API">Compression Streams API</h2>
<p>Compression Streams API は、データの圧縮や展開を行うための API で、特定の実行環境に依存せずに使うことができる。<br/>
gzip 形式や deflate 形式に対応している。</p>
<p>この記事では、この API を使って文字列の可逆圧縮を実装する。</p>
<p>注意点として、記事執筆時点ではブラウザ対応はそれほど行われておらず、例えば Firefox は対応していない。<br/>
<a href="https://developer.mozilla.org/ja/docs/Web/API/CompressionStream/CompressionStream#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%83%BC%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7">https://developer.mozilla.org/ja/docs/Web/API/CompressionStream/CompressionStream#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%83%BC%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7</a></p>
<h2 id="文字列を-Stream-に変換し圧縮する">文字列を Stream に変換し圧縮する</h2>
<p>Compression Streams API はその名の通り Stream を対象とした API である。<br/>
そのため、まずは対象のデータ(今回の場合は文字列)を Stream に変換する必要がある。</p>
<p><code>Blob</code>のインスタンスメソッドである<code>stream</code>を使うことで Stream を取得できるので、まず最初に文字列を<code>Blob</code>に変換する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> originalString <span class="synStatement">=</span> <span class="synConstant">"a"</span><span class="synStatement">;</span>
<span class="synType">const</span> blob <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>originalString<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synType">const</span> stream: ReadableStream<span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span> <span class="synStatement">=</span> blob.stream<span class="synStatement">();</span>
</pre>
<p>そしてその Stream を<code>CompressionStream</code>インスタンスに渡す(パイプでつなげる)ことで、圧縮が行われる。<br/>
<code>CompressionStream</code>コンストラクタには圧縮形式を渡すのだが、この記事では<code>deflate-raw</code>を渡すことにする。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> originalString <span class="synStatement">=</span> <span class="synConstant">"a"</span><span class="synStatement">;</span>
<span class="synType">const</span> blob <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>originalString<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synType">const</span> stream: ReadableStream<span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span> <span class="synStatement">=</span> blob.stream<span class="synStatement">();</span>
<span class="synType">const</span> compressedStream <span class="synStatement">=</span> stream.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> CompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
</pre>
<p>圧縮そのものはこれで完了である。</p>
<h2 id="圧縮した-Stream-を文字列やバイナリに変換する">圧縮した Stream を文字列やバイナリに変換する</h2>
<p>データを圧縮してそれで終わり、というケースは考えづらく、基本的には圧縮したデータを保存するなり何らかの処理に使うなりしたいはずである。<br/>
その際に Stream のままではなく別のデータとして扱いたいことも多い。</p>
<p>様々な方法があると思うが、<code>Response</code>を使えば簡単に文字列やバイナリとしてデータを取得できる。<br/>
具体的には、<code>Response</code>インスタンスの<code>arrayBuffer</code>メソッドや<code>text</code>メソッドを使う。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> originalString <span class="synStatement">=</span> <span class="synConstant">"a"</span><span class="synStatement">;</span>
<span class="synType">const</span> blob <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>originalString<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synType">const</span> stream: ReadableStream<span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span> <span class="synStatement">=</span> blob.stream<span class="synStatement">();</span>
<span class="synType">const</span> compressedStream <span class="synStatement">=</span> stream.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> CompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synType">const</span> res <span class="synStatement">=</span> <span class="synStatement">new</span> Response<span class="synStatement">(</span>compressedStream<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"arrayBuffer"</span> <span class="synStatement">in</span> res<span class="synStatement">);</span> <span class="synComment">// true</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"text"</span> <span class="synStatement">in</span> res<span class="synStatement">);</span> <span class="synComment">// true</span>
</pre>
<h2 id="DecompressionStream-で展開する">DecompressionStream で展開する</h2>
<p>圧縮された Stream を展開するには<code>DecompressionStream</code>を使う。<br/>
使い方は<code>CompressionStream</code>と同じで、インスタンスを作成する際にデータ形式を選び、それに対象の Stream をパイプすればよい。この際、圧縮時と異なるデータ形式を選択すると当然エラーになるので注意する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> decompressedStream <span class="synStatement">=</span> compressedStream.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> DecompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
</pre>
<p>展開によって元のデータに戻っているのか確認するが、その前にまず、圧縮によってデータが変換されていることを確認する。</p>
<p>以下のコードでは圧縮を行った Stream と行っていない Stream のバイナリデータを調べているが、確かに変換が行われている。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> stringToStream <span class="synStatement">=</span> <span class="synStatement">(</span>target: <span class="synType">string</span><span class="synStatement">)</span>: ReadableStream<span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> blob <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>target<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synStatement">return</span> blob.stream<span class="synStatement">();</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> streamToUint8Array <span class="synStatement">=</span> <span class="synStatement">async</span> <span class="synStatement">(</span>
stream: ReadableStream<span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span>
<span class="synStatement">)</span>: <span class="synSpecial">Promise</span><span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(await</span> <span class="synStatement">new</span> Response<span class="synStatement">(</span>stream<span class="synStatement">)</span>.arrayBuffer<span class="synStatement">());</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> originalString <span class="synStatement">=</span> <span class="synConstant">"a"</span><span class="synStatement">;</span>
<span class="synType">const</span> originalStream <span class="synStatement">=</span> stringToStream<span class="synStatement">(</span>originalString<span class="synStatement">);</span>
<span class="synComment">// 圧縮を行わない場合: 97</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`圧縮を行わない場合: </span><span class="synSpecial">${</span><span class="synStatement">await</span> streamToUint8Array(originalStream)<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">);</span>
<span class="synType">const</span> compressedStream <span class="synStatement">=</span> stringToStream<span class="synStatement">(</span>originalString<span class="synStatement">)</span>.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> CompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synComment">// 圧縮を行った場合: 74,4,0,0,0,255,255,3,0</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`圧縮を行った場合: </span><span class="synSpecial">${</span><span class="synStatement">await</span> streamToUint8Array(compressedStream)<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">);</span>
</pre>
<p>なお、圧縮後に却ってデータが長くなってしまっているが、これは元のデータが極端に短いからであり、後述するように元データの長さが一定以上であれば基本的には圧縮効果を得られる。</p>
<p><code>97</code>から<code>74,4,0,0,0,255,255,3,0</code>に変換されたデータが<code>DecompressionStream</code>で元に戻るのか確認する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> stringToStream <span class="synStatement">=</span> <span class="synStatement">(</span>target: <span class="synType">string</span><span class="synStatement">)</span>: ReadableStream<span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> blob <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>target<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synStatement">return</span> blob.stream<span class="synStatement">();</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> streamToUint8Array <span class="synStatement">=</span> <span class="synStatement">async</span> <span class="synStatement">(</span>
stream: ReadableStream<span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span>
<span class="synStatement">)</span>: <span class="synSpecial">Promise</span><span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(await</span> <span class="synStatement">new</span> Response<span class="synStatement">(</span>stream<span class="synStatement">)</span>.arrayBuffer<span class="synStatement">());</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> originalString <span class="synStatement">=</span> <span class="synConstant">"a"</span><span class="synStatement">;</span>
<span class="synType">const</span> originalStream <span class="synStatement">=</span> stringToStream<span class="synStatement">(</span>originalString<span class="synStatement">);</span>
<span class="synComment">// 元データ: 97</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`元データ: </span><span class="synSpecial">${</span><span class="synStatement">await</span> streamToUint8Array(originalStream)<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">);</span>
<span class="synType">const</span> compressedStream <span class="synStatement">=</span> stringToStream<span class="synStatement">(</span>originalString<span class="synStatement">)</span>.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> CompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synType">const</span> decompressedStream <span class="synStatement">=</span> compressedStream.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> DecompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synComment">// 圧縮後に展開したデータ: 97</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
<span class="synConstant">`圧縮後に展開したデータ: </span><span class="synSpecial">${</span><span class="synStatement">await</span> streamToUint8Array(decompressedStream)<span class="synSpecial">}</span><span class="synConstant">`</span>
<span class="synStatement">);</span>
</pre>
<p>確かに元に戻っている。</p>
<h2 id="圧縮後の文字列を展開して元の文字列を手に入れる">圧縮後の文字列を展開して元の文字列を手に入れる</h2>
<p>Compression Streams API を使えば Stream を可逆圧縮できることが分かった。<br/>
しかしこの記事でそもそもやりたかったことは、任意の文字列を圧縮し、圧縮された文字列のリテラル表現から元の文字列を復元できるようにすることである。</p>
<p>そのためには、圧縮後の Stream を文字列に変換しなければならない。</p>
<p>既述の通り<code>Response</code>インスタンスには<code>text</code>メソッドがあるので、それを使えば簡単に文字列が手に入る。<br/>
だがこの方法では上手くいかない。</p>
<p>試しに<code>a</code>を圧縮し、圧縮結果を<code>text</code>メソッドで取得してみる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> stringToStream <span class="synStatement">=</span> <span class="synStatement">(</span>target: <span class="synType">string</span><span class="synStatement">)</span>: ReadableStream<span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> blob <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>target<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synStatement">return</span> blob.stream<span class="synStatement">();</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> compressedStream <span class="synStatement">=</span> stringToStream<span class="synStatement">(</span><span class="synConstant">"a"</span><span class="synStatement">)</span>.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> CompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synIdentifier">{</span> a: <span class="synStatement">await(new</span> Response<span class="synStatement">(</span>compressedStream<span class="synStatement">)</span>.text<span class="synStatement">())</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synComment">// { a: "J\x04\x00\x00\x00��\x03\x00" }</span>
</pre>
<p><code>J\x04\x00\x00\x00��\x03\x00</code>という文字列が手に入る。<br/>
だがこの文字列を<code>a</code>に戻すことはできない。置換文字である<code>�</code>が含まれてしまっているからだ。</p>
<p>他のデータと同様に文字列もまた、プログラムの内部では<code>0</code>と<code>1</code>の羅列として扱われる。その羅列を規定のルールに基づいて文字列に変換することで、文字列としての表現を得る。ルールに則っていないデータの場合、上手く文字列に変換することができない。<br/>
そして Unicode において表示不可能な文字を表現するのが、置換文字である。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2022%2F10%2F30%2F190527" title="Unicode における置換文字(replacement character)について - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://numb86-tech.hatenablog.com/entry/2022/10/30/190527">numb86-tech.hatenablog.com</a></cite></p>
<p>どんなデータであっても、文字列として有効でない場合は全て<code>�</code>と表示されてしまうため、もともとどのようなデータであったかという情報は失われてしまう。</p>
<p>CompressionStream はただデータの圧縮を行うだけであり、圧縮後のデータが文字列として有効かどうかは何も考慮しないため、このようなことが発生してしまう。<br/>
そのため、圧縮されたデータをそのまま文字列に変換するのではなく、まずは文字列として有効な形式に変換する必要がある。</p>
<p>今回は、圧縮後のバイト列の要素を一つずつ文字列に変換することで、文字列として有効なデータに変換する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> stringToStream <span class="synStatement">=</span> <span class="synStatement">(</span>target: <span class="synType">string</span><span class="synStatement">)</span>: ReadableStream<span class="synStatement"><</span><span class="synSpecial">Uint8Array</span><span class="synStatement">></span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> blob <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>target<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synStatement">return</span> blob.stream<span class="synStatement">();</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> compressedStream <span class="synStatement">=</span> stringToStream<span class="synStatement">(</span><span class="synConstant">"a"</span><span class="synStatement">)</span>.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> CompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synType">const</span> arrayBuffer <span class="synStatement">=</span> <span class="synStatement">await(new</span> Response<span class="synStatement">(</span>compressedStream<span class="synStatement">)</span>.arrayBuffer<span class="synStatement">());</span>
<span class="synComment">// 圧縮後のデータの Uint8Array による表現を手に入れる</span>
<span class="synType">const</span> bytes <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span>arrayBuffer<span class="synStatement">);</span>
<span class="synType">let</span> binaryString <span class="synStatement">=</span> <span class="synConstant">""</span><span class="synStatement">;</span>
<span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement"><</span> bytes.byteLength<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span>
<span class="synComment">// Uint8Array の各要素は 0..255 の範囲内になるので、それを Code Unit として利用すれば必ず有効な文字列を得られる</span>
binaryString <span class="synStatement">+=</span> <span class="synSpecial">String</span>.fromCharCode<span class="synStatement">(</span>bytes<span class="synIdentifier">[</span>i<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synIdentifier">{</span> binaryString <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synComment">// { binaryString: "J\x04\x00\x00\x00ÿÿ\x03\x00" }</span>
</pre>
<p>置換文字を消すことができた。これで、元の文字列に復元することが可能になる。</p>
<p>ここまでに行った処理を整理すると、以下のようになる。</p>
<ol>
<li>圧縮したい文字列を Stream に変換する</li>
<li><code>CompressionStream</code>で Stream を圧縮する</li>
<li>圧縮された Stream の<code>Uint8Array</code>による表現を手に入れる</li>
<li><code>Uint8Array</code>の各要素を Code Unit として使って文字列を作る</li>
</ol>
<p>なので、それと逆の処理を行えば元の文字列が手に入る。</p>
<ol>
<li>展開したい文字列の Code Unit を取得して、それを使った<code>Uint8Array</code>を作る</li>
<li><code>Uint8Array</code>を Stream に変換する</li>
<li><code>DecompressionStream</code>で Stream を展開する</li>
<li>展開された Stream を文字列に変換する</li>
</ol>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> str <span class="synStatement">=</span> <span class="synConstant">"J</span><span class="synSpecial">\x04\x00\x00\x00</span><span class="synConstant">ÿÿ</span><span class="synSpecial">\x03\x00</span><span class="synConstant">"</span><span class="synStatement">;</span>
<span class="synType">const</span> bytes <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span>str.length<span class="synStatement">);</span>
<span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement"><</span> str.length<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span>
bytes<span class="synIdentifier">[</span>i<span class="synIdentifier">]</span> <span class="synStatement">=</span> str.charCodeAt<span class="synStatement">(</span>i<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synType">const</span> stream <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>bytes<span class="synIdentifier">]</span><span class="synStatement">)</span>.stream<span class="synStatement">();</span>
<span class="synType">const</span> decompressedStream <span class="synStatement">=</span> stream.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> DecompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(await</span> <span class="synStatement">new</span> Response<span class="synStatement">(</span>decompressedStream<span class="synStatement">)</span>.text<span class="synStatement">());</span> <span class="synComment">//a</span>
</pre>
<h2 id="Base64-を使う">Base64 を使う</h2>
<p>これで圧縮と展開を行えるようになったので、この時点で当初の目的は達成できている。<br/>
だが、<code>a</code>の圧縮結果である<code>J\x04\x00\x00\x00ÿÿ\x03\x00</code>を見れば分かるように、<code>U+0004</code>や<code>U+0000</code>のような制御文字も含まれてしまっている。</p>
<p>圧縮後の文字列の用途によっては、制御文字が含まれていると都合が悪かったり扱いづらかったりすることがある。<br/>
そのような場合、圧縮後の文字列を Base64 でエンコードすることで、制御文字を取り除くことができる。<br/>
その場合はもちろん、展開する際にまずデコードする必要がある。</p>
<p>上記で説明した圧縮方法では、各文字は全て Latin1 の範囲内に収まるので、<code>btoa</code>でエンコードできる。</p>
<p>Base64 や Latin1 、<code>btoa</code>については以下の記事を参照。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2022%2F12%2F04%2F134350" title="JavaScript で Base64 - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://numb86-tech.hatenablog.com/entry/2022/12/04/134350">numb86-tech.hatenablog.com</a></cite></p>
<h2 id="成果物">成果物</h2>
<p>ここまでの内容を関数としてまとめたのが以下。Base64 によるエンコードも行うようにしている。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> compress <span class="synStatement">=</span> <span class="synStatement">async</span> <span class="synStatement">(</span>target: <span class="synType">string</span><span class="synStatement">)</span>: <span class="synSpecial">Promise</span><span class="synStatement"><</span><span class="synType">string</span><span class="synStatement">></span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> arrayBufferToBinaryString <span class="synStatement">=</span> <span class="synStatement">(</span>arrayBuffer: <span class="synSpecial">ArrayBuffer</span><span class="synStatement">)</span>: <span class="synType">string</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> bytes <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span>arrayBuffer<span class="synStatement">);</span>
<span class="synType">let</span> binaryString <span class="synStatement">=</span> <span class="synConstant">""</span><span class="synStatement">;</span>
<span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement"><</span> bytes.byteLength<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span>
binaryString <span class="synStatement">+=</span> <span class="synSpecial">String</span>.fromCharCode<span class="synStatement">(</span>bytes<span class="synIdentifier">[</span>i<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">return</span> binaryString<span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> blob <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>target<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synType">const</span> stream <span class="synStatement">=</span> blob.stream<span class="synStatement">();</span>
<span class="synType">const</span> compressedStream <span class="synStatement">=</span> stream.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> CompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synType">const</span> buf <span class="synStatement">=</span> <span class="synStatement">await</span> <span class="synStatement">new</span> Response<span class="synStatement">(</span>compressedStream<span class="synStatement">)</span>.arrayBuffer<span class="synStatement">();</span>
<span class="synType">const</span> binaryString <span class="synStatement">=</span> arrayBufferToBinaryString<span class="synStatement">(</span>buf<span class="synStatement">);</span>
<span class="synType">const</span> encodedByBase64 <span class="synStatement">=</span> btoa<span class="synStatement">(</span>binaryString<span class="synStatement">);</span>
<span class="synStatement">return</span> encodedByBase64<span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> decompress <span class="synStatement">=</span> <span class="synStatement">async</span> <span class="synStatement">(</span>target: <span class="synType">string</span><span class="synStatement">)</span>: <span class="synSpecial">Promise</span><span class="synStatement"><</span><span class="synType">string</span><span class="synStatement">></span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> binaryStringToBytes <span class="synStatement">=</span> <span class="synStatement">(</span>str: <span class="synType">string</span><span class="synStatement">)</span>: <span class="synSpecial">Uint8Array</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> bytes <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span>str.length<span class="synStatement">);</span>
<span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement"><</span> str.length<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span>
bytes<span class="synIdentifier">[</span>i<span class="synIdentifier">]</span> <span class="synStatement">=</span> str.charCodeAt<span class="synStatement">(</span>i<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">return</span> bytes<span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> decodedByBase64 <span class="synStatement">=</span> atob<span class="synStatement">(</span>target<span class="synStatement">);</span>
<span class="synType">const</span> bytes <span class="synStatement">=</span> binaryStringToBytes<span class="synStatement">(</span>decodedByBase64<span class="synStatement">);</span>
<span class="synType">const</span> stream <span class="synStatement">=</span> <span class="synStatement">new</span> Blob<span class="synStatement">(</span><span class="synIdentifier">[</span>bytes<span class="synIdentifier">]</span><span class="synStatement">)</span>.stream<span class="synStatement">();</span>
<span class="synType">const</span> decompressedStream <span class="synStatement">=</span> stream.pipeThrough<span class="synStatement">(</span>
<span class="synStatement">new</span> DecompressionStream<span class="synStatement">(</span><span class="synConstant">"deflate-raw"</span><span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synStatement">return</span> <span class="synStatement">await</span> <span class="synStatement">new</span> Response<span class="synStatement">(</span>decompressedStream<span class="synStatement">)</span>.text<span class="synStatement">();</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<p>動作確認してみると、圧縮も展開も上手くいっている。<br/>
元の文字列の内容にもよるのだが、基本的には、文字列が長くなればなるほど圧縮効果は大きくなる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> originalString <span class="synStatement">=</span> <span class="synConstant">`<!DOCTYPE html></span>
<span class="synConstant"><html lang="en"></span>
<span class="synConstant"><head></span>
<span class="synConstant"> <meta charset="UTF-8"></span>
<span class="synConstant"> <title>Document</title></span>
<span class="synConstant"></head></span>
<span class="synConstant"><body></span>
<span class="synConstant"> <p>abc</p></span>
<span class="synConstant"> <p>abc</p></span>
<span class="synConstant"> <p>abc</p></span>
<span class="synConstant"> <p>123</p></span>
<span class="synConstant"> <p>123</p></span>
<span class="synConstant"> <p>123</p></span>
<span class="synConstant"></body></span>
<span class="synConstant"></html></span>
<span class="synConstant">`</span><span class="synStatement">;</span>
<span class="synType">const</span> compressed <span class="synStatement">=</span> <span class="synStatement">await</span> compress<span class="synStatement">(</span>originalString<span class="synStatement">);</span>
<span class="synComment">// hI69EoIwDMf3PkXs7vXUxSF0EV11wMExlJxw1xYOwsDbA+0DMOV+Sf4feCrfj+r3eUIrwVuF+wBP8V9ojnpfMDVWAWBgIXAtjRNLob/V63zX6SCdeLZl7+bAUdBkVmiyFOu+WdLjYKl2aIZDuFxvB4Amu24hqfcKAAD//wMA</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>compressed<span class="synStatement">);</span>
<span class="synType">const</span> decompressed <span class="synStatement">=</span> <span class="synStatement">await</span> decompress<span class="synStatement">(</span>
<span class="synConstant">"hI69EoIwDMf3PkXs7vXUxSF0EV11wMExlJxw1xYOwsDbA+0DMOV+Sf4feCrfj+r3eUIrwVuF+wBP8V9ojnpfMDVWAWBgIXAtjRNLob/V63zX6SCdeLZl7+bAUdBkVmiyFOu+WdLjYKl2aIZDuFxvB4Amu24hqfcKAAD//wMA"</span>
<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>decompressed <span class="synStatement">===</span> originalString<span class="synStatement">);</span> <span class="synComment">// true</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">${</span>originalString.length<span class="synSpecial">}</span><span class="synConstant"> -> </span><span class="synSpecial">${</span>compressed.length<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">);</span> <span class="synComment">// 200 -> 168</span>
</pre>
<h2 id="ブラウザ環境での動作確認">ブラウザ環境での動作確認</h2>
<p><code>compress</code>と<code>decompress</code>から型注釈を取り除いて Google Chrome (<code>108.0.5359.124</code>) で実行すると、問題なく動く。<br/>
ただ、 Deno とは Compression Streams API の実装が異なるらしく、圧縮した結果が異なる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// Deno</span>
<span class="synStatement">await</span> compress<span class="synStatement">(</span><span class="synConstant">"a"</span><span class="synStatement">);</span> <span class="synComment">// SgQAAAD//wMA</span>
<span class="synComment">// Google Chrome</span>
<span class="synStatement">await</span> compress<span class="synStatement">(</span><span class="synConstant">"a"</span><span class="synStatement">);</span> <span class="synComment">// 'SwQA'</span>
</pre>
<p>その一方で、どちらの環境で圧縮した文字列であっても、どちらの環境でも復元できる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// `SwQA`も`SgQAAAD//wMA`も decompress に渡すと`a`になる</span>
<span class="synComment">// Deno でも Chrome でもどちらでもそうなる</span>
<span class="synStatement">await</span> decompress<span class="synStatement">(</span><span class="synConstant">"SwQA"</span><span class="synStatement">);</span> <span class="synComment">// a</span>
<span class="synStatement">await</span> decompress<span class="synStatement">(</span><span class="synConstant">"SgQAAAD//wMA"</span><span class="synStatement">);</span> <span class="synComment">// a</span>
</pre>
<p>Firefox (<code>108.0.1</code>) で<code>compress</code>を実行しようとすると、<code>CompressionStream</code>が存在しないためエラーになる。</p>
<pre class="code" data-lang="" data-unlink>ReferenceError: CompressionStream is not defined</pre>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://qiita.com/dojyorin/items/55d3435580688df62762">【JavaScript】実行環境のネイティブDEFLATE実装を使えるCompressionStreamsAPI - Qiita</a></li>
<li><a href="https://zenn.dev/takaodaze/articles/74ac1684a7d1d2">JavaScript での バイナリ → base64 変換</a></li>
</ul>
numb_86
Next.js の skipTrailingSlashRedirect で trailing slash の設定をカスタマイズする
hatenablog://entry/4207112889952320322
2023-01-14T20:22:43+09:00
2023-01-14T20:22:43+09:00 Next.js のv13.1.0で追加されたskipTrailingSlashRedirectを使うことで、 trailing slash に関する挙動を自由に設定できる。 この記事では、skipTrailingSlashRedirectによって具体的にどのようなことが可能になったのかを見ていく。 動作確認はv13.1.1で行った。 環境構築 まずは Next.js の環境構築から。 $ yarn create next-app sample --ts こうするとsampleというディレクトリが作られるので、そこに移動して作業を進めていく。 まず、next.config.jsのbasePath…
<p>Next.js の<code>v13.1.0</code>で追加された<code>skipTrailingSlashRedirect</code>を使うことで、 trailing slash に関する挙動を自由に設定できる。<br/>
この記事では、<code>skipTrailingSlashRedirect</code>によって具体的にどのようなことが可能になったのかを見ていく。<br/>
動作確認は<code>v13.1.1</code>で行った。</p>
<h2 id="環境構築">環境構築</h2>
<p>まずは Next.js の環境構築から。</p>
<pre class="code" data-lang="" data-unlink>$ yarn create next-app sample --ts</pre>
<p>こうすると<code>sample</code>というディレクトリが作られるので、そこに移動して作業を進めていく。</p>
<p>まず、<code>next.config.js</code>の<code>basePath</code>に<code>"/app"</code>を指定する。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">/** @type {import('next').NextConfig} */</span>
<span class="synStatement">const</span> nextConfig = <span class="synIdentifier">{</span>
reactStrictMode: <span class="synConstant">true</span>,
basePath: <span class="synConstant">"/app"</span>, <span class="synComment">// これを追加</span>
<span class="synIdentifier">}</span>;
module.exports = nextConfig;
</pre>
<p>そして以下の内容の<code>pages/foo.tsx</code>を作成する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synStatement">function</span> Foo<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement"><</span>h1<span class="synStatement">></span>Foo Page<span class="synStatement"><</span>/h1<span class="synStatement">>;</span>
<span class="synIdentifier">}</span>
</pre>
<p>これで下準備が完了。</p>
<h2 id="trailing-slash">trailing slash</h2>
<p><code>skipTrailingSlashRedirect</code>はその名の通り trailing slash に関するリダイレクトをスキップする機能なのだが、これを理解するためにはまず、 trailing slash について理解している必要がある。</p>
<p>trailing slash とは URL の末尾についている<code>/</code>のこと。<br/>
Next.js では、 trailing slash をつけるかどうか設定することができ、設定内容に応じて Next.js がリダイレクト処理を行ってくれる。</p>
<p>デフォルトでは<code>/</code>を取り除くようになっているので、確認してみる。</p>
<p>trailing slash のない URL にリクエストを送ると、ステータスコード<code>200</code>が返ってくる。</p>
<pre class="code" data-lang="" data-unlink>$ curl -IL http://localhost:3000/app
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/foo
HTTP/1.1 200 OK</pre>
<p>そして trailing slash のある URL にリクエストを送ると、<code>/</code>を取り除いた URL へとリダイレクトされる。</p>
<pre class="code" data-lang="" data-unlink>$ curl -IL http://localhost:3000/app/
HTTP/1.1 308 Permanent Redirect
Location: /app
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/foo/
HTTP/1.1 308 Permanent Redirect
Location: /app/foo
HTTP/1.1 200 OK</pre>
<p>trailing slash をつけたい場合は、<code>next.config.js</code>で<code>trailingSlash</code>を有効にする。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">/** @type {import('next').NextConfig} */</span>
<span class="synStatement">const</span> nextConfig = <span class="synIdentifier">{</span>
reactStrictMode: <span class="synConstant">true</span>,
basePath: <span class="synConstant">"/app"</span>,
trailingSlash: <span class="synConstant">true</span>, <span class="synComment">// これを追加</span>
<span class="synIdentifier">}</span>;
module.exports = nextConfig;
</pre>
<p>サーバを再起動して、動作確認してみる。</p>
<pre class="code" data-lang="" data-unlink>$ curl -IL http://localhost:3000/app/
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/foo/
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app
HTTP/1.1 308 Permanent Redirect
Location: /app/
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/foo
HTTP/1.1 308 Permanent Redirect
Location: /app/foo/
HTTP/1.1 200 OK</pre>
<p>先程とは逆に trailing slash のある URL へとリダイレクトされるようになる。</p>
<h2 id="trailingSlash-の問題">trailingSlash の問題</h2>
<p>このように<code>trailingSlash</code>オプションで trailing slash の有無を選べるのだが、ひとつ問題があり、 URL によって設定を変えることができない。<br/>
そのため例えば、ルートパスのみ trailing slash を付与してそれ以外のパスでは付与しない、ということはできない。</p>
<p>Next.js には、リクエストを受け取ってリダイレクト処理などを行える middleware という機能があるが、この機能を使っても上記の問題は解決しない。<br/>
<code>trailingSlash</code>によるリダイレクト処理が、 middleware よりも先に実行されてしまうためである。</p>
<p>実際に middleware を用意して確認してみる。</p>
<p>ルートディレクトリ(今回の例では<code>sample</code>)に<code>middleware.ts</code>を作って以下のように書くと、 middleware が受け取ったリクエストのパスがログに流れるようになる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synStatement">type</span> <span class="synIdentifier">{</span> NextRequest <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"next/server"</span><span class="synStatement">;</span>
<span class="synStatement">export</span> <span class="synStatement">function</span> middleware<span class="synStatement">(</span>request: NextRequest<span class="synStatement">)</span>: <span class="synType">void</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> requestPathname <span class="synStatement">=</span> <span class="synStatement">new</span> URL<span class="synStatement">(</span>request.url<span class="synStatement">)</span>.pathname<span class="synStatement">;</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>requestPathname<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<p>この状態で trailing slash のない URL にリクエストを送っても、 middleware にリクエストが渡される前にリダイレクトされてしまい、ログには trailing slash 付きのパスのみが流れてくる。</p>
<pre class="code" data-lang="" data-unlink>/app/
/app/foo/</pre>
<p><code>trailingSlash</code>を無効にしても同じで、そうすると今度は trailing slash がつかないパスのみが流れてくるようになる。</p>
<p>このように、 middleware にリクエストが渡されるよりも先に、<code>trailingSlash</code>によるリダイレクトが実行されてしまう。<br/>
そのため middleware による制御もできず、 Next.js で trailing slash を柔軟に設定することは難しかった。</p>
<h2 id="skipTrailingSlashRedirect-による解決">skipTrailingSlashRedirect による解決</h2>
<p>だが<code>v13.1.0</code>からは、<code>skipTrailingSlashRedirect</code>によってこの問題を解決できるようになった。</p>
<p>まず、<code>next.config.js</code>で<code>skipTrailingSlashRedirect</code>を有効にしてサーバを再起動する。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">/** @type {import('next').NextConfig} */</span>
<span class="synStatement">const</span> nextConfig = <span class="synIdentifier">{</span>
reactStrictMode: <span class="synConstant">true</span>,
basePath: <span class="synConstant">"/app"</span>,
skipTrailingSlashRedirect: <span class="synConstant">true</span>, <span class="synComment">// trailingSlash を削除してこれを追加する</span>
<span class="synIdentifier">}</span>;
module.exports = nextConfig;
</pre>
<p>こうすることで、<code>trailingSlash</code>に関するリダイレクトが何も行われなくなる。<br/>
trailing slash をつけるためのリダイレクトも、取り除くためのリダイレクトも、行われない。</p>
<pre class="code" data-lang="" data-unlink>$ curl -IL http://localhost:3000/app
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/foo
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/foo/
HTTP/1.1 200 OK</pre>
<p>あとは middleware で任意の処理を追加すればよい。<br/>
今回は root path のみ<code>/</code>をつけるようにする。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> NextResponse <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"next/server"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synStatement">type</span> <span class="synIdentifier">{</span> NextRequest <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"next/server"</span><span class="synStatement">;</span>
<span class="synStatement">export</span> <span class="synStatement">function</span> middleware<span class="synStatement">(</span>request: NextRequest<span class="synStatement">)</span>: NextResponse <span class="synIdentifier">{</span>
<span class="synType">const</span> requestPathname <span class="synStatement">=</span> <span class="synStatement">new</span> URL<span class="synStatement">(</span>request.url<span class="synStatement">)</span>.pathname<span class="synStatement">;</span>
<span class="synComment">// basePath の値は /app</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> basePath <span class="synIdentifier">}</span> <span class="synStatement">=</span> request.nextUrl<span class="synStatement">;</span>
<span class="synComment">// /app にリクエストがあった場合は /app/ にリダイレクトする</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>requestPathname <span class="synStatement">===</span> basePath<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> NextResponse.redirect<span class="synStatement">(new</span> URL<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">${</span>basePath<span class="synSpecial">}</span><span class="synConstant">/`</span><span class="synStatement">,</span> request.url<span class="synStatement">));</span>
<span class="synIdentifier">}</span>
<span class="synComment">// /app/ 以外で末尾が / になっているパスの場合は、末尾から / を取り除いたパスにリダイレクトする</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>requestPathname.endsWith<span class="synStatement">(</span><span class="synConstant">"/"</span><span class="synStatement">)</span> <span class="synConstant">&&</span> requestPathname <span class="synStatement">!==</span> <span class="synConstant">`</span><span class="synSpecial">${</span>basePath<span class="synSpecial">}</span><span class="synConstant">/`</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> NextResponse.redirect<span class="synStatement">(</span>
<span class="synStatement">new</span> URL<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">${</span>requestPathname.slice(<span class="synConstant">0</span>, <span class="synConstant">-1</span>)<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">,</span> request.url<span class="synStatement">)</span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synComment">// それ以外のパスはそのまま処理を続ける</span>
<span class="synStatement">return</span> NextResponse.next<span class="synStatement">();</span>
<span class="synIdentifier">}</span>
</pre>
<p>リクエストを送ってみると、意図した通りの挙動になっている。</p>
<pre class="code" data-lang="" data-unlink>$ curl -IL http://localhost:3000/app
HTTP/1.1 307 Temporary Redirect
location: /app/
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/foo
HTTP/1.1 200 OK
$ curl -IL http://localhost:3000/app/foo/
HTTP/1.1 307 Temporary Redirect
location: /app/foo
HTTP/1.1 200 OK</pre>
<h2 id="skipMiddlewareUrlNormalize">skipMiddlewareUrlNormalize</h2>
<p>上述したように curl でリクエストを送ると上手く動くのだが、実はブラウザで開くと問題が起きる。<br/>
<code>http://localhost:3000/app/foo</code>は問題ないのだが、<code>http://localhost:3000/app/</code>にアクセスすると発生する。</p>
<p>ブラウザの開発者ツールで通信状況を確認してみると、<code>http://localhost:3000/app/_next/data/development/index.json</code>へのリクエストが無限に発生し続けていることが分かる。</p>
<p>なぜこのようなことが起こるのかというと、この JSON ファイルへのリクエストを middleware で扱う際に URL の正規化が行われ、<code>/app</code>へのリクエストと解釈されてしまうのである。<br/>
その結果、この JSON ファイルにアクセスしようとする度にリダイレクトが発生し、それがいつまでも繰り返されるという状況になってしまったのである。</p>
<p>URL の正規化を無効にするには、<code>skipTrailingSlashRedirect</code>と同様に<code>v13.1.0</code>で導入された<code>skipMiddlewareUrlNormalize</code>を有効にする必要がある。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">/** @type {import('next').NextConfig} */</span>
<span class="synStatement">const</span> nextConfig = <span class="synIdentifier">{</span>
reactStrictMode: <span class="synConstant">true</span>,
basePath: <span class="synConstant">"/app"</span>,
skipTrailingSlashRedirect: <span class="synConstant">true</span>,
skipMiddlewareUrlNormalize: <span class="synConstant">true</span>, <span class="synComment">// これを追加する</span>
<span class="synIdentifier">}</span>;
module.exports = nextConfig;
</pre>
<p>これで正規化が行われなくなり、リクエストが繰り返される事象が解決される。</p>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://nextjs.org/docs/advanced-features/middleware">Advanced Features: Middleware | Next.js</a></li>
</ul>
numb_86
JavaScript で Base64
hatenablog://entry/4207112889942103404
2022-12-04T13:43:50+09:00
2022-12-04T13:43:50+09:00 この記事では Base64 やbtoa、そしてbtoaの挙動を理解するために必要な Latin1 について説明していく。 この記事に出てくるコードの動作確認は以下の環境で行った。 Deno 1.28.3 TypeScript 4.8.3 概要 Base64 はデータのエンコード方式の一種。 全てのデータをa~z(26 文字)、A~Z(26 文字)、0~9(10 文字)、そして+と/を合わせた計 64 文字、さらにそこに=を組み合わせたテキストで表現する。 そうすることで、扱えるデータに制限のある環境において、その制限を超えたデータを扱えるようになる。 例えば電子メールではテキストデータしか扱え…
<p>この記事では Base64 や<code>btoa</code>、そして<code>btoa</code>の挙動を理解するために必要な Latin1 について説明していく。</p>
<p>この記事に出てくるコードの動作確認は以下の環境で行った。</p>
<ul>
<li>Deno 1.28.3</li>
<li>TypeScript 4.8.3</li>
</ul>
<h2 id="概要">概要</h2>
<p>Base64 はデータのエンコード方式の一種。<br/>
全てのデータを<code>a</code>~<code>z</code>(26 文字)、<code>A</code>~<code>Z</code>(26 文字)、<code>0</code>~<code>9</code>(10 文字)、そして<code>+</code>と<code>/</code>を合わせた計 64 文字、さらにそこに<code>=</code>を組み合わせたテキストで表現する。</p>
<p>そうすることで、扱えるデータに制限のある環境において、その制限を超えたデータを扱えるようになる。<br/>
例えば電子メールではテキストデータしか扱えないが、バイナリデータを Base64 にエンコードしてしまうことで、問題なくバイナリデータを送信できるようになる。あとは受信側で Base64 をデコードすればよい。<br/>
他にも、 Data URL でバイナリデータを扱う際にも Base64 が使われている。</p>
<h2 id="btoa-と-atob">btoa と atob</h2>
<p>JavaScript には、文字列を Base64 でエンコード・デコードするための関数が予め用意されている。</p>
<p>エンコードには<code>btoa</code>を使い、デコードには<code>atob</code>を使う。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synSpecial">console</span>.log<span class="synStatement">(</span>btoa<span class="synStatement">(</span><span class="synConstant">"a"</span><span class="synStatement">));</span> <span class="synComment">// YQ==</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>atob<span class="synStatement">(</span><span class="synConstant">"YQ=="</span><span class="synStatement">));</span> <span class="synComment">// a</span>
</pre>
<p>だが<code>btoa</code>は、あらゆる文字をエンコードできるわけではない。むしろエンコードできない文字のほうが多い。<br/>
例えば日本語を渡すとエラーになってしまう。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// error: Uncaught InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range.</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>btoa<span class="synStatement">(</span><span class="synConstant">"あ"</span><span class="synStatement">));</span>
</pre>
<h2 id="Latin1-とは何か">Latin1 とは何か</h2>
<p>エラーメッセージは<code>The string to be encoded contains characters outside of the Latin1 range.</code>。「エンコードする文字列に、Latin1 の範囲外の文字が含まれている」とのこと。<br/>
これにより、<code>btoa</code>には Latin1 範囲内の文字しか渡せないことが分かる。</p>
<p>では Latin1 とは何か。</p>
<p>ISO/IEC 8859 という文字コードのパート 1 の通称が、 Latin1 。<br/>
ISO/IEC 8859 は 8 ビット 256 文字の文字コードであり、複数の「パート」がある。256 文字のうち前半 128 文字は全パート共通で、その部分は ASCII 文字コードと同一になっている。そして後半 128 文字は、パートによって内容が異なる。<br/>
目的に応じてパートを選択して使用するようになっているのだが、そのうちのパート 1 が Latin1 である。</p>
<p>Latin1 で使える文字は以下で見れる。<br/>
<a href="https://ja.wikipedia.org/wiki/ISO/IEC_8859-1#%E7%AC%A6%E5%8F%B7%E8%A1%A8">https://ja.wikipedia.org/wiki/ISO/IEC_8859-1#%E7%AC%A6%E5%8F%B7%E8%A1%A8</a></p>
<p>そして Unicode の最初の 256 個の Code Point(<code>U+0000..U+00FF</code>)は Latin1 と同じ内容になっている(Unicode や Code Point については<a href="https://numb86-tech.hatenablog.com/entry/2022/10/23/190637">この記事</a>で説明している)。<br/>
JavaScript は Unicode を採用しているため、 JavaScript の文脈においては「Latin1」と言ったときは、その範囲の文字を指すと考えてよい。</p>
<p><code>U+0100</code>以降の文字を<code>btoa</code>に渡してみると、確かに失敗する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">let</span> str <span class="synStatement">=</span> <span class="synSpecial">String</span>.fromCodePoint<span class="synStatement">(</span><span class="synConstant">0x61</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str<span class="synStatement">);</span> <span class="synComment">// a</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>btoa<span class="synStatement">(</span>str<span class="synStatement">));</span> <span class="synComment">// YQ==</span>
str <span class="synStatement">=</span> <span class="synSpecial">String</span>.fromCodePoint<span class="synStatement">(</span><span class="synConstant">0xff</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str<span class="synStatement">);</span> <span class="synComment">// ÿ</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>btoa<span class="synStatement">(</span>str<span class="synStatement">));</span> <span class="synComment">// /w==</span>
str <span class="synStatement">=</span> <span class="synSpecial">String</span>.fromCodePoint<span class="synStatement">(</span><span class="synConstant">0x100</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str<span class="synStatement">);</span> <span class="synComment">// Ā</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>btoa<span class="synStatement">(</span>str<span class="synStatement">));</span> <span class="synComment">// error: Uncaught InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range.</span>
</pre>
<p>このため、 Latin1 範囲外の文字を<code>btoa</code>でエンコードするためには、その文字を何らかの方法で Latin1 範囲内の文字に変換する必要がある。そしてそれは、元の文字に戻せる方法でなければならない。そうしないとデコードできない。</p>
<p>具体的な処理の流れは以下のようになる。</p>
<ol>
<li>文字を Latin1 範囲内の文字に変換する</li>
<li>変換後の文字を<code>btoa</code>に渡す</li>
<li>Base64 でエンコードされた文字が手に入る</li>
</ol>
<p>デコードして元の文字を手に入れるときは、これと逆のことをやればよい。</p>
<ol>
<li>エンコードされた文字を<code>atob</code>に渡す</li>
<li><code>atob</code>から渡された文字に対して、「文字を Latin1 範囲内の文字に変換する」で行ったのと逆の処理を行う</li>
<li>元の文字が手に入る</li>
</ol>
<h2 id="encodeURIComponent">encodeURIComponent</h2>
<p>Latin1 の文字列を手に入れるための手段のひとつとして、<code>encodeURIComponent</code>がある。</p>
<p>この関数は以下の文字以外の全ての文字をエスケープする。</p>
<pre class="code" data-lang="" data-unlink>A-Z a-z 0-9 - _ . ! ~ * ' ( )</pre>
<p>アルファベットや数字はもちろん、記号も全て Latin1 の範囲内に収まっている。<br/>
つまり Latin1 範囲外の文字は全てエスケープ対象となる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// 2d</span>
<span class="synComment">// 5f</span>
<span class="synComment">// 2e</span>
<span class="synComment">// 21</span>
<span class="synComment">// 7e</span>
<span class="synComment">// 2a</span>
<span class="synComment">// 27</span>
<span class="synComment">// 28</span>
<span class="synComment">// 29</span>
<span class="synSpecial">Array</span>.<span class="synStatement">from(</span><span class="synConstant">"-_.!~*'()"</span><span class="synStatement">)</span>.forEach<span class="synStatement">((</span>item<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>item.codePointAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">)</span>?.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">));</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<p>そして<code>encodeURIComponent</code>の返り値は<code>%XX</code>(<code>XX</code>は 16 進数)という形式の文字列になる。<code>%</code>も Latin1 なので(Code Point <code>U+0025</code>)、返り値は必ず Latin1 範囲内の文字列になることが保証される。</p>
<p>そのため<code>encodeURIComponent</code>を使えば、 Latin1 範囲外の文字を含む文字列を、 Latin1 範囲内に収まる形に変換できる。あとはそれを<code>btoa</code>に渡せばよい。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> original <span class="synStatement">=</span> <span class="synConstant">"あ"</span><span class="synStatement">;</span>
<span class="synType">const</span> latin1 <span class="synStatement">=</span> encodeURIComponent<span class="synStatement">(</span>original<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>latin1<span class="synStatement">);</span> <span class="synComment">// %E3%81%82</span>
<span class="synType">const</span> base64 <span class="synStatement">=</span> btoa<span class="synStatement">(</span>latin1<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>base64<span class="synStatement">);</span> <span class="synComment">// JUUzJTgxJTgy</span>
<span class="synComment">// 逆の処理を行うと元の文字列が手に入る</span>
<span class="synType">const</span> decoded <span class="synStatement">=</span> atob<span class="synStatement">(</span>base64<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>decoded<span class="synStatement">);</span> <span class="synComment">// %E3%81%82</span>
<span class="synType">const</span> restore <span class="synStatement">=</span> decodeURIComponent<span class="synStatement">(</span>decoded<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>restore<span class="synStatement">);</span> <span class="synComment">// あ</span>
</pre>
<h2 id="ArrayBuffer-を活用する">ArrayBuffer を活用する</h2>
<p>ArrayBuffer を使うことでも、 Latin1 範囲内の文字列に変換することができる(ArrayBuffer そのものについては<a href="https://numb86-tech.hatenablog.com/entry/2019/05/12/134052">この記事</a>で説明している)。</p>
<p>JavaScript では文字を、符号なし 16 ビット整数を使って表現している(UTF-16)。<br/>
これを、符号なし 8 ビット整数による表現に変えてしまう。そうするとひとつひとつの要素は<code>0..255</code>の範囲内に収まるので、それを Code Point として利用すればそれは必ず Latin1 の範囲内に文字になる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> str <span class="synStatement">=</span> <span class="synConstant">"あ"</span><span class="synStatement">;</span>
<span class="synComment">// 符号なし 16 ビット整数を入れていくための「箱」として Uint16Array を用意する</span>
<span class="synType">const</span> ta16 <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint16Array</span><span class="synStatement">(</span>str.length<span class="synStatement">);</span>
<span class="synComment">// JavaScript の Code Unit は符号なし 16 ビット整数なので、そのまま Uint16Array に格納できる</span>
<span class="synType">const</span> codeUnit <span class="synStatement">=</span> str.charCodeAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>codeUnit<span class="synStatement">);</span> <span class="synComment">// 12354</span>
ta16<span class="synIdentifier">[</span><span class="synConstant">0</span><span class="synIdentifier">]</span> <span class="synStatement">=</span> codeUnit<span class="synStatement">;</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>ta16<span class="synStatement">);</span> <span class="synComment">// Uint16Array(1) [ 12354 ]</span>
<span class="synComment">// Uint8Array による表現を手に入れる</span>
<span class="synType">const</span> ta8 <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span>ta16.buffer<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>ta8<span class="synStatement">);</span> <span class="synComment">// Uint8Array(2) [ 66, 48 ]</span>
<span class="synComment">// Uint8Array の各要素は 0..255 の範囲内になるので、それを Code Point として利用すれば必ず Latin1 の範囲内の文字列になる</span>
<span class="synType">const</span> latin1 <span class="synStatement">=</span> <span class="synSpecial">String</span>.fromCodePoint<span class="synStatement">(</span>...ta8<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>latin1<span class="synStatement">);</span> <span class="synComment">// B0</span>
<span class="synComment">// 問題なく btoa を使える</span>
<span class="synType">const</span> encoded <span class="synStatement">=</span> btoa<span class="synStatement">(</span>latin1<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>encoded<span class="synStatement">);</span> <span class="synComment">// QjA=</span>
</pre>
<p>元の文字列を得るには逆の処理を行う。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> decoded <span class="synStatement">=</span> atob<span class="synStatement">(</span><span class="synConstant">"QjA="</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>decoded<span class="synStatement">);</span> <span class="synComment">// B0</span>
<span class="synType">const</span> ta8 <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span>decoded.length<span class="synStatement">);</span>
ta8<span class="synIdentifier">[</span><span class="synConstant">0</span><span class="synIdentifier">]</span> <span class="synStatement">=</span> decoded.charCodeAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">);</span>
ta8<span class="synIdentifier">[</span><span class="synConstant">1</span><span class="synIdentifier">]</span> <span class="synStatement">=</span> decoded.charCodeAt<span class="synStatement">(</span><span class="synConstant">1</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>ta8<span class="synStatement">);</span> <span class="synComment">// Uint8Array(2) [ 66, 48 ]</span>
<span class="synType">const</span> ta16 <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint16Array</span><span class="synStatement">(</span>ta8.buffer<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>ta16<span class="synStatement">);</span> <span class="synComment">// Uint16Array(1) [ 12354 ]</span>
<span class="synType">const</span> original <span class="synStatement">=</span> <span class="synSpecial">String</span>.fromCodePoint<span class="synStatement">(</span>...ta16<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>original<span class="synStatement">);</span> <span class="synComment">// あ</span>
</pre>
<p>上記の内容を任意の文字列に対して行えるように関数化したものが、以下になる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> toBase64 <span class="synStatement">=</span> <span class="synStatement">(</span>str: <span class="synType">string</span><span class="synStatement">)</span>: <span class="synType">string</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> ta16 <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint16Array</span><span class="synStatement">(</span>str.length<span class="synStatement">);</span>
<span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement"><</span> ta16.length<span class="synStatement">;</span> i <span class="synStatement">+=</span> <span class="synConstant">1</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
ta16<span class="synIdentifier">[</span>i<span class="synIdentifier">]</span> <span class="synStatement">=</span> str.charCodeAt<span class="synStatement">(</span>i<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synType">const</span> latin1 <span class="synStatement">=</span> <span class="synSpecial">String</span>.fromCodePoint<span class="synStatement">(</span>...<span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span>ta16.buffer<span class="synStatement">));</span>
<span class="synStatement">return</span> btoa<span class="synStatement">(</span>latin1<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> fromBase64 <span class="synStatement">=</span> <span class="synStatement">(</span>encoded: <span class="synType">string</span><span class="synStatement">)</span>: <span class="synType">string</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> decoded <span class="synStatement">=</span> atob<span class="synStatement">(</span>encoded<span class="synStatement">);</span>
<span class="synType">const</span> ta8 <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span>decoded.length<span class="synStatement">);</span>
<span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement"><</span> ta8.length<span class="synStatement">;</span> i <span class="synStatement">+=</span> <span class="synConstant">1</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
ta8<span class="synIdentifier">[</span>i<span class="synIdentifier">]</span> <span class="synStatement">=</span> decoded.charCodeAt<span class="synStatement">(</span>i<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">return</span> <span class="synSpecial">String</span>.fromCodePoint<span class="synStatement">(</span>...<span class="synStatement">new</span> <span class="synSpecial">Uint16Array</span><span class="synStatement">(</span>ta8.buffer<span class="synStatement">));</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>toBase64<span class="synStatement">(</span><span class="synConstant">"あ"</span><span class="synStatement">));</span> <span class="synComment">// QjA=</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>fromBase64<span class="synStatement">(</span><span class="synConstant">"QjA="</span><span class="synStatement">));</span> <span class="synComment">// あ</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>toBase64<span class="synStatement">(</span><span class="synConstant">"abcあいうえお🐶"</span><span class="synStatement">));</span> <span class="synComment">// YQBiAGMAQjBEMEYwSDBKMD3YNtw=</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>fromBase64<span class="synStatement">(</span><span class="synConstant">"YQBiAGMAQjBEMEYwSDBKMD3YNtw="</span><span class="synStatement">));</span> <span class="synComment">// abcあいうえお🐶</span>
</pre>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://developer.mozilla.org/ja/docs/Web/API/btoa">WindowOrWorkerGlobalScope.btoa() - Web API | MDN</a></li>
<li><a href="https://e-words.jp/w/ISO-IEC_8859.html">ISO/IEC 8859とは - 意味をわかりやすく - IT用語辞典 e-Words</a></li>
<li><a href="https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF_(Unicode">ブロック (Unicode) - Wikipedia</a>)</li>
</ul>
numb_86
『優れた技術者の集まる会社にする方法 ソフトウェア開発者採用ガイド』を読んだ
hatenablog://entry/4207112889940090678
2022-11-27T21:03:09+09:00
2022-11-27T21:03:09+09:00 前回読んだ『Joel on Software』の Joel Spolsky が、ソフトウェア開発者の採用について論じた一冊。 自身が優秀な開発者であり経営者でもある Joel が、多くのソフトウェア開発者が採用に対して何となく感じていることを平易な文章で明快に説明していく。 詳しく調べてはいないが、本書の内容も基本的に Joel on Software で公開されている記事をまとめたものだと思う。 例えば「第 2 章 優れた開発者を見つけるには」は、以下の記事を翻訳したものになっている。 エッセイ集なので採用業務について体系立てて論じているわけではないが、それゆえに非常に読みやすい。しかし内容…
<p><a href="https://numb86-tech.hatenablog.com/entry/2022/11/19/191646">前回読んだ『Joel on Software』</a>の Joel Spolsky が、ソフトウェア開発者の採用について論じた一冊。<br/>
自身が優秀な開発者であり経営者でもある Joel が、多くのソフトウェア開発者が採用に対して何となく感じていることを平易な文章で明快に説明していく。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.shoeisha.co.jp%2Fbook%2Fdetail%2F9784798115825" title="優れた技術者の集まる会社にする方法 ソフトウェア開発者採用ガイド | 翔泳社" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p>詳しく調べてはいないが、本書の内容も基本的に <a href="https://www.joelonsoftware.com/">Joel on Software</a> で公開されている記事をまとめたものだと思う。<br/>
例えば「第 2 章 優れた開発者を見つけるには」は、以下の記事を翻訳したものになっている。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.joelonsoftware.com%2F2006%2F09%2F06%2Ffinding-great-developers-2%2F" title="Finding Great Developers" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p>エッセイ集なので採用業務について体系立てて論じているわけではないが、それゆえに非常に読みやすい。しかし内容は薄くない。<br/>
「分かってはいるが実践は困難」「この基準を満たす開発者がどれだけいるんだ」と思いたくなる内容もあるが、主張している内容はいずれも真っ当なものばかり。<br/>
Joel 自身が本書の内容を実践しており、その結果 Stack Overflow や Trello など日本でも知られているサービスが複数生まれているため、とにかく説得力がある。<br/>
ソフトウェア開発者が読んでも面白いが、開発者にとっては常識であったり感覚的に理解しているような事柄について丁寧に論じているので、非開発者が読むとより大きな示唆を得られるかもしれない。</p>
<p>本書の内容を自分なりに要約すると、以下のようになる。</p>
<p>ソフトウェアの機能や品質がそのまま競争力になるような事業の場合、優れたソフトウェア開発者を採用できるかが事業の成否を左右する。<br/>
優れた開発者でなければ作れないものがある。凡庸な人間をいくら集めても優れた人材の代替にはならないし、そういう人たちに膨大な時間を与えたところで結果は同じ。<br/>
そのため、候補者が本当に優れた開発者なのかを注意深く慎重に見極めなければならない。<br/>
そして優れた開発者は引く手数多なので、基本的に採用市場には出てこないし、出会えたとしても自社に来てくれるかは分からない。彼らはいくつも選択肢を持っているのだから。<br/>
さらに、幸運にも優れた開発者が入社してくれたところで、彼らをうんざりさせるような政治や彼らを軽んじるようなマネージメントを繰り返していれば、彼らの能力を活かすことはできないし、いずれは去っていくだろう。<br/>
このように、優れた開発者を雇って事業を成功させるためには、いくつものハードルを越えなければならない。</p>
<p>候補者と企業、どちらも同じ課題を抱えており、それぞれに「強者」と「弱者」が生まれているんだろうなと、本書を読んで感じた。<br/>
お互いに「誰でもいい」わけではなく、だからこそ、たくさんの選択肢を持つ「強者」と、苦しくなる一方の「弱者」が生まれてしまう。<br/>
市況の変化によってトレンドは変わるが、基本的な構造は変わらない。</p>
<p>例えば本書には「一括送信の履歴書は死にものぐるいの徴候に思える」という表現が出てくる。<br/>
志望動機が薄くて汎用的な内容の履歴書を提出されると、本当にこの仕事をしたいと思っているのか疑わしくなってしまう、という話なのだが、これは企業側にも言える。<br/>
パーソナライズされておらず誰にでも言えるような内容のスカウトメールを一括送信しているようでは、候補者からよい印象を持たれる可能性は低い。「死にものぐるい」と映るだろう。<br/>
そして「死にものぐるい」が採用市場で高く評価されることはない。魅力があり「強者」である企業や候補者は多くの選択肢を持っており、そんなことしないからだ。</p>
<p>どうやって自分を知ってもらうか、自分を見てもらうか、にも同じことが言える。<br/>
採用広報の重要性と難しさが本書で出てくる。どんなに魅力を持った企業であっても、それを上手く発信していかないと目に留めてもらえないのが現実で、まず知ってもらうということのハードルが高い。<br/>
そして候補者も同じ状況にある。たくさんの応募の中から、どうやって自分を見つけてもらうのか、どうやって面接を受けるチャンスを掴むのか。</p>
<p>面接には多くのリソースを必要とするので、希望する人全てと面接するのは企業にとって現実的ではない。<br/>
他の業務との兼ね合いもあるので、自然と効率性を求めることになる。そのため、フィルタリングが行わる。<br/>
本書では履歴書と電話面接によるフィルタリングを紹介しているが、履歴書によるフィルタリングでは、「(人によっては)何年も前の過去の話」がそれなりに大きなウェイトを占めている。<br/>
詳しくは実際に読んでもらいたいが、選考プロセスが厳しい大学や企業に入ったことがあるか、大学の成績はよいか、大学対抗プログラミングコンテンストに出場したことはあるか、など。これらによって、どの候補者と優先的に面接するかを決めている。</p>
<p>高校を中退しており無名文系私大卒の私にとっては、かなり厳しい内容だ。経歴だけでなくアクティビティについても同様で、若い頃を引きこもりとして無為に過ごしたため、セキュリティ・キャンプに参加したこともなければ、未踏ジュニアに応募したこともない。<br/>
そして残念ながら、過去の経歴で判断するのはそれなりに妥当だろうなとは思う。情報工学を修めているかでソフトウェア開発者の履歴書を選別するのは、別に間違ってはいないと思う。</p>
<p>とはいえ、それらがフィルタリング条件として最適というわけではなく、企業と候補者の双方に機会損失が生まれているのも事実だと思う。<br/>
履歴書から読み取れる情報は少なく、書類上の評価が高くても能力が低い候補者はいるし、その逆のケースもあると、本書にも書かれている。<br/>
企業にとっても候補者にとっても、出会いたい相手に出会うのは難しいのが現状と言える。今はまだ何のアイディアもないが、もっと上手い仕組みを作れるとよいなとは思っている。</p>
numb_86
『Joel on Software』を読んだ
hatenablog://entry/4207112889937229989
2022-11-19T19:16:46+09:00
2022-11-19T19:16:46+09:00 Microsoft での勤務経験を持ち Stack Overflow の創業者でもある Joel Spolsky によるエッセイ集。 Joel は自身が運営するウェブサイト Joel on Software で多数の記事を公開しており、その一部を掲載したのが本書。 ひとつひとつの章がかなり短い(長いものでも 20 ページくらい、短いものだと 4 ページほど)ので気軽に読めるし、各章は独立しているので興味のある部分だけ読むこともできる。 技術そのものについて解説している技術書ではなく、ソフトウェア開発やソフトウェア産業についての著者の考えが書かれており、 Paul Graham の『ハッカーと画…
<p>Microsoft での勤務経験を持ち Stack Overflow の創業者でもある Joel Spolsky によるエッセイ集。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ohmsha.co.jp%2Fbook%2F9784274066306%2F" title="Joel on Software | Ohmsha" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p> Joel は自身が運営するウェブサイト <a href="https://www.joelonsoftware.com/">Joel on Software</a> で多数の記事を公開しており、その一部を掲載したのが本書。</p>
<p>ひとつひとつの章がかなり短い(長いものでも 20 ページくらい、短いものだと 4 ページほど)ので気軽に読めるし、各章は独立しているので興味のある部分だけ読むこともできる。</p>
<p>技術そのものについて解説している技術書ではなく、ソフトウェア開発やソフトウェア産業についての著者の考えが書かれており、 Paul Graham の『ハッカーと画家』にテイストが近いかもしれない。<br/>
無料で公開されているエッセイ集をまとめたもの、というのも『ハッカーと画家』に似ている。</p>
<p>本書に収録されているのは 2000 年から 2004 年に書かれた記事なので、さすがに古さを感じるものもある。<br/>
未来予測のような内容も含まれるので、現在の視点から「答え合わせ」してみるのも面白いかもしれない。</p>
<p>以下、特に面白かった章の要約。</p>
<ul>
<li><p><strong> 第 20 章 採用面接ゲリラガイド </strong></p>
<ul>
<li>候補者は、明らかに水準に到達していない人、何とも言えない人、スーパースターの 3 種類いる
<ul>
<li>何とも言えない人とスーパースターを区別して前者を採用しないことが重要</li>
</ul>
</li>
<li>特定のタスクを非常によくこなせるとしても、他のチームではあまり上手くいかないような候補者も不採用
<ul>
<li>変化の速い世界なので、どのようなタスクでもこなせる人間が必要</li>
</ul>
</li>
<li>分からない、境界線上だ、というケースは全て不採用
<ul>
<li>どっちつかずの候補者はすべて機械的に不採用にする</li>
</ul>
</li>
<li>なぜなら、まずい候補者を採用するよりは、いい候補者を落とすほうがずっとマシだから</li>
<li>優れた候補者を見つけるのがいかに難しく感じられても、基準を下げてはいけない</li>
<li>採用するべきなのは、頭がよくて物事を成し遂げる人</li>
<li>頭のよさを見分けるために、候補者の頭のよさを示せるような状況を作ることが重要
<ul>
<li>自分の話ばかりして候補者に話す時間を与えない面接官はダメ</li>
<li>頭のよさを「たくさんの事実を知っていること」だと思ってクイズショーを展開する面接官もダメ</li>
</ul>
</li>
<li>ソフトウェアチームが採用するべきなのは、特定のスキルセットを持っている人ではなく、資質を持っている人
<ul>
<li>スキルセットはすぐ陳腐化するのだから、どんな新技術でも学べる人を雇ったほうがいい</li>
</ul>
</li>
<li>その人を知るための一番の方法は、その人に話をさせること
<ul>
<li>そのために自由回答式の質問と問題を与える</li>
</ul>
</li>
<li>面接する候補者の情報はできるだけ入れないようにする
<ul>
<li>どうしてもバイアスが掛かってしまうから</li>
</ul>
</li>
</ul>
</li>
<li><p><strong> 第 24 章 あなたが絶対すべきでないこと PART I </strong></p>
<ul>
<li>プログラムをスクラッチで書き直すのは、最悪の戦略的誤り</li>
<li>プログラマが既存のコードを捨てたいと思いがちなのは、プログラムは書くより読むほうが難しいため</li>
<li>しかし、古いコードは様々な面で新しいコードよりも利点がある
<ul>
<li>古いコードは既に動いている</li>
<li>古いコードは歴史のなかでテストされている
<ul>
<li>多くのバグが見つかり、それが修正されている</li>
<li>実世界のなかで何週間も使われたからこそバグが見つかった</li>
</ul>
</li>
</ul>
</li>
<li>コードを捨てることは積み重ねられたバグフィックスを捨てることを意味する</li>
</ul>
</li>
<li><p><strong> 第 26 章 漏れのある抽象化の法則 </strong></p>
<ul>
<li>抽象化とは、複雑なことが行われている中身を隠して単純化すること</li>
<li>抽象化によって、中身の詳細を知らなくても利用できるようになる</li>
<li>例えば TCP は IP の上に構築されているが、抽象化によって IP のことを意識することなく TCP を利用できる</li>
<li>しかし TCP より下層の部分で何か問題が発生すると、 TCP は機能しなくなる
<ul>
<li>何からの理由で IP パケットが全く届かなくなったり、ネットワークケーブルが切断されてしまったり</li>
</ul>
</li>
<li>このような状態を Joel は「漏れのある抽象化」と呼んでいる</li>
<li>自明ではない抽象化はすべて、多かれ少なかれ漏れがある</li>
<li>普段は上手くいっていても、時折漏れ出してきて、問題が発生する
<ul>
<li>メモリの仕組みを知らなくても問題なくコードを書けるかもしれないが、知っていないことで、ものすごくパフォーマンスに問題のあるコードを書いてしまうかもしれない
<ul>
<li>コンピュータの仕組みを完全に抽象化することはできず、このように漏れ出してくることがある</li>
</ul>
</li>
<li>SQL のクエリでも似たようなことが起こり得る</li>
</ul>
</li>
<li>抽象化によって隠されている「下層」について知らないと、抽象化に失敗して下層が漏れ出してきたときに対応できない</li>
</ul>
</li>
<li><p><strong> 第 33 章 ビッグマック 対 裸のシェフ </strong></p>
<ul>
<li>物事を上手くやるためには才能が必要</li>
<li>だが才能をスケールさせるのは困難</li>
<li>そこで、才能ある者がルールを作り、凡庸な人間がそれに従うというやり方で、スケールさせようとする</li>
<li>しかしその結果として得られる成果物の品質は低い
<ul>
<li>一定であり安定しているかもしれないが、低い</li>
</ul>
</li>
<li>ルールや手順は、平時においては上手く機能するかもしれない
<ul>
<li>だが状況の変化に対応できない</li>
<li>ルールを作った才能ある人達なら、対応できるだろう。だがルールに従うしかできない人間では、対応できない。対応できるだけの能力がないからこそ、ルールを必要としそれに従っているわけだから。</li>
</ul>
</li>
<li>これが、企業の勃興と衰退が繰り返される理由
<ul>
<li>硬直化し時代に対応できない大企業を、若い才能が追い抜く。しかし才能はスケールしない。そこで才能ある者がルールを作り、凡庸な人間にそれに従わせる。最初は上手くいくが、状況変化に対応できず勢いを失っていく。</li>
</ul>
</li>
<li>この過ちを犯さないためには、優秀な人間を雇うことに執着しなければならない</li>
</ul>
</li>
</ul>
numb_86
Unicode における置換文字(replacement character)について
hatenablog://entry/4207112889930154537
2022-10-30T19:05:27+09:00
2022-10-30T19:05:27+09:00 この記事では、 Unicode において表示不可能な文字を表現する「置換文字」について説明する。 この記事に出てくるコードの動作確認は以下の環境で行った。 Deno 1.26.0 TypeScript 4.8.3 概要 Unicode において、表示しようとした文字が何らかの理由で表示不可能なとき、黒い菱形に白いクエスチョンマークが書かれた文字が表示される。 「�」がそうなのだが、環境によっては表示されずカギカッコの中が空白になっているかもしれないので、画像も載せておく。 この文字を「置換文字」と呼ぶ。 サロゲートペアとして不正なケース 文字が表示不可能な例として、サロゲートペアとして正しくな…
<p>この記事では、 Unicode において表示不可能な文字を表現する「置換文字」について説明する。</p>
<p>この記事に出てくるコードの動作確認は以下の環境で行った。</p>
<ul>
<li>Deno 1.26.0</li>
<li>TypeScript 4.8.3</li>
</ul>
<h2 id="概要">概要</h2>
<p>Unicode において、表示しようとした文字が何らかの理由で表示不可能なとき、黒い菱形に白いクエスチョンマークが書かれた文字が表示される。<br />
「�」がそうなのだが、環境によっては表示されずカギカッコの中が空白になっているかもしれないので、画像も載せておく。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20221023/20221023203950.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<p>この文字を「置換文字」と呼ぶ。</p>
<h2 id="サロゲートペアとして不正なケース">サロゲートペアとして不正なケース</h2>
<p>文字が表示不可能な例として、サロゲートペアとして正しくないケースがある。</p>
<p>サロゲートペアや Code Point の概要は以前書いたので、必要ならこちらを読んで欲しい。<br />
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2022%2F10%2F23%2F190637" title="JavaScript における文字コードの初歩 - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://numb86-tech.hatenablog.com/entry/2022/10/23/190637">numb86-tech.hatenablog.com</a></cite></p>
<p>Code Point のうち一部分はサロゲートペアに使うものとして予め定められており、単独で文字を表現することはない。<br />
具体的には<code>U+D800</code>からの<code>U+DFFF</code>の 2048 文字分がそれに該当する。<br />
これらの Code Point は代用符号位置と呼ばれる。<br />
さらに、代用符号位置は前半(<code>U+D800</code>〜<code>U+DBFF</code>)と後半(<code>U+DC00</code>〜<code>U+DFFF</code>)に分かれており、前半を上位サロゲート、後半を下位サロゲートとして使うことも決まっている。</p>
<p>上記のルールに反した場合、無効なデータと見做され置換文字が表示される。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// 単独の代用符号位置で文字を表現することはできない</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`\u{d800}`</span><span class="synStatement">);</span> <span class="synComment">// �</span>
<span class="synComment">// dc00 が上位サロゲートに来ることはないし、 d800 が下位サロゲートに来ることもない</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">\udc00\ud800</span><span class="synConstant">`</span><span class="synStatement">);</span> <span class="synComment">// ��</span>
</pre>
<h2 id="置換文字の-Code-Point-や-Code-Unit-について">置換文字の Code Point や Code Unit について</h2>
<p>表示不可能な文字は全て<code>�</code>として表示しようというだけの話なので、表示結果が同じ<code>�</code>だったとしても、 Code Point や Code Unit が異なれば、それは別の文字である。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> x <span class="synStatement">=</span> <span class="synConstant">`</span><span class="synSpecial">\udc00</span><span class="synConstant">`</span><span class="synStatement">;</span>
<span class="synType">const</span> y <span class="synStatement">=</span> <span class="synConstant">`</span><span class="synSpecial">\udc01</span><span class="synConstant">`</span><span class="synStatement">;</span>
<span class="synType">const</span> z <span class="synStatement">=</span> <span class="synConstant">`</span><span class="synSpecial">\udc02</span><span class="synConstant">`</span><span class="synStatement">;</span>
<span class="synComment">// 三文字とも � と表示されるが……</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>x<span class="synStatement">,</span> y<span class="synStatement">,</span> z<span class="synStatement">);</span> <span class="synComment">// � � �</span>
<span class="synComment">// 全て別々の文字である</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>x <span class="synStatement">===</span> y<span class="synStatement">);</span> <span class="synComment">// false</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>x <span class="synStatement">===</span> z<span class="synStatement">);</span> <span class="synComment">// false</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>y <span class="synStatement">===</span> z<span class="synStatement">);</span> <span class="synComment">// false</span>
</pre>
<p>ちなみに、<code>�</code>そのものの Code Point は<code>U+FFFD</code>である。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"�"</span>.codePointAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">)</span>?.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">));</span> <span class="synComment">// fffd</span>
</pre>
numb_86
JavaScript における文字コードの初歩
hatenablog://entry/4207112889930090781
2022-10-23T19:06:37+09:00
2022-10-23T19:06:37+09:00 この記事では、 JavaScript で文字コードを扱う際に知っておくべき概念である Code Point や Code Unit、サロゲートペア、といったものについて説明していく。 また、具体的にそれらの概念を使ってどのようにコードを書いていくのかについても扱う。 この記事に出てくるコードの動作確認は以下の環境で行った。 Deno 1.26.0 TypeScript 4.8.3 Code Point (符号位置) プログラムで文字を表現する方法は複数あるが、 JavaScript では Unicode という方法を採用している。 Unicode ではあらゆる文字に対して一意の値を割り振ること…
<p>この記事では、 JavaScript で文字コードを扱う際に知っておくべき概念である Code Point や Code Unit、サロゲートペア、といったものについて説明していく。<br />
また、具体的にそれらの概念を使ってどのようにコードを書いていくのかについても扱う。</p>
<p>この記事に出てくるコードの動作確認は以下の環境で行った。</p>
<ul>
<li>Deno 1.26.0</li>
<li>TypeScript 4.8.3</li>
</ul>
<h2 id="Code-Point-符号位置">Code Point (符号位置)</h2>
<p>プログラムで文字を表現する方法は複数あるが、 JavaScript では Unicode という方法を採用している。<br />
Unicode ではあらゆる文字に対して一意の値を割り振ることを目的としており、この値のことを Code Point (符号位置)という。</p>
<p>Code Point は 16 進数の非負整数で、文章中で表記するときは接頭辞として<code>U+</code>をつける。<br />
例えば<code>A</code>という文字の Code Point は<code>U+0041</code>、<code>あ</code>は<code>U+3042</code>、<code>🐶</code>は<code>U+1f436</code>として定義されている。</p>
<p>ES2015 で追加された<code>codePointAt</code>メソッドを使うと、任意の文字列リテラルの Code Point を取得できる。<br />
数値リテラルが返ってくるので、 16 進数による表記を得たい場合は<code>toString</code>で変換する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> str <span class="synStatement">=</span> <span class="synConstant">"Aあ🐶"</span><span class="synStatement">;</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str.codePointAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">));</span> <span class="synComment">// 65</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str.codePointAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">)</span>?.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">));</span> <span class="synComment">// 41</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str.codePointAt<span class="synStatement">(</span><span class="synConstant">1</span><span class="synStatement">)</span>?.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">));</span> <span class="synComment">//3042</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str.codePointAt<span class="synStatement">(</span><span class="synConstant">2</span><span class="synStatement">)</span>?.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">));</span> <span class="synComment">//1f436</span>
</pre>
<p><code>`\u{CodePoint}`</code>と書くことで Code Point から文字列リテラルを得ることもできる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`\u{41}`</span><span class="synStatement">);</span> <span class="synComment">// A</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`\u{3042}`</span><span class="synStatement">);</span> <span class="synComment">// あ</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`\u{1f436}`</span><span class="synStatement">);</span> <span class="synComment">// 🐶</span>
</pre>
<p>ES2015 で追加された静的メソッドである<code>String.fromCodePoint</code>を使うことでも Code Point から文字列リテラルへの変換を行える。この方法だと Code Point を変数に入れて使うこともできる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> codePoint <span class="synStatement">=</span> <span class="synConstant">0x41</span><span class="synStatement">;</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synSpecial">String</span>.fromCodePoint<span class="synStatement">(</span>codePoint<span class="synStatement">));</span> <span class="synComment">// A</span>
</pre>
<h2 id="Code-Unit符号単位">Code Unit(符号単位)</h2>
<p>文字を実際にコンピュータで扱うためには、 Code Point をさらに Code Unit(符号単位)に変換する必要がある。<br />
Code Unit はプログラムにおける文字の内部表現であり、これを元に<code>0</code>と<code>1</code>の羅列であるバイト列に変換することで、コンピュータが文字をスムーズに扱えるようになる。</p>
<p>Unicode の Code Point を Code Unit に変換する方法はいくつか定義されているが、 JavaScript では UTF-16 という方法を採用している。<br />
UTF-16 では、 Code Unit を符号なし 16 ビット整数を使って表現する。そのため、 JavaScript の内部においては文字列は、符号なし 16 ビット整数が並んでいるものとして扱われる。</p>
<p>Code Unit も Code Point 同様に 16 進数で表記されることが多い。</p>
<p>符号なし 16 ビット整数の範囲は<code>0000</code>から<code>FFFF</code>。<br />
16 ビットは 16 桁の 2 進数なので<code>2 ^ 16 = 65536</code>であり、<code>FFFF</code>の 10 進数表記が<code>65535</code>であるためそうなる。</p>
<p><code>charCodeAt</code>メソッドで、任意の文字列リテラルの Code Unit を取得できる。<br />
これも<code>codePointAt</code>と同様に数値リテラルが返ってくる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> str <span class="synStatement">=</span> <span class="synConstant">"Aあ"</span><span class="synStatement">;</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str.charCodeAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">));</span> <span class="synComment">// 65</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str.charCodeAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">)</span>?.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">));</span> <span class="synComment">// 41</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>str.charCodeAt<span class="synStatement">(</span><span class="synConstant">1</span><span class="synStatement">)</span>?.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">));</span> <span class="synComment">//3042</span>
</pre>
<p>Code Unit から文字列リテラルに変換する方法も用意されており、<code>`\uCodeUnit`</code>と<code>String.fromCharCode</code>がある。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">\u0041</span><span class="synConstant">`</span><span class="synStatement">);</span> <span class="synComment">// A</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">\u3042</span><span class="synConstant">`</span><span class="synStatement">);</span> <span class="synComment">// あ</span>
<span class="synType">const</span> codeUnit <span class="synStatement">=</span> <span class="synConstant">0x41</span><span class="synStatement">;</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synSpecial">String</span>.fromCharCode<span class="synStatement">(</span>codeUnit<span class="synStatement">));</span> <span class="synComment">// A</span>
</pre>
<h2 id="サロゲートペア">サロゲートペア</h2>
<p><code>A</code>と<code>あ</code>は Code Point と Code Unit が同じだったが、<code>🐶</code>は異なる。<br />
そもそも<code>A</code>や<code>あ</code>とは異なり Code Unit が 2 つある。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> check <span class="synStatement">=</span> <span class="synStatement">(</span>str: <span class="synType">string</span><span class="synStatement">)</span>: <span class="synType">void</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> length <span class="synStatement">=</span> str.length<span class="synStatement">;</span>
<span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement"><</span> length<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>i<span class="synStatement">,</span> str.charCodeAt<span class="synStatement">(</span>i<span class="synStatement">)</span>.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">));</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synComment">// 0 41</span>
check<span class="synStatement">(</span><span class="synConstant">"A"</span><span class="synStatement">);</span>
<span class="synComment">// 0 3042</span>
check<span class="synStatement">(</span><span class="synConstant">"あ"</span><span class="synStatement">);</span>
<span class="synComment">// 0 d83d</span>
<span class="synComment">// 1 dc36</span>
check<span class="synStatement">(</span><span class="synConstant">"🐶"</span><span class="synStatement">);</span>
</pre>
<p>先程、符号なし 16 ビット整数では<code>65536</code>個の数を扱えると書いたが、 Unicode が扱う文字の数はそれをゆうに超える。<br />
つまり符号なし 16 ビット整数では、 Unicode が扱う全ての文字を表現することが出来ないのである。<br />
そのため UTF-16 では、 Code Unit をふたつ組み合わせてひとつの文字を表現する方法を導入した。<br />
そのような文字をサロゲートペアと呼ぶ。<br />
<code>🐶</code>もサロゲートペアである。そのため、 Code Unit がふたつあった。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// U+1f436(🐶)は d83d と dc36 の組み合わせで表現される</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">\ud83d\udc36</span><span class="synConstant">`</span><span class="synStatement">);</span> <span class="synComment">// 🐶</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synSpecial">String</span>.fromCharCode<span class="synStatement">(</span><span class="synConstant">0xd83d</span><span class="synStatement">,</span> <span class="synConstant">0xdc36</span><span class="synStatement">));</span> <span class="synComment">// 🐶</span>
</pre>
<p>一方で<code>A</code>と<code>あ</code>はひとつの Code Unit で表現されており、サロゲートペアではない。<br />
このように UTF-16 においては、ひとつの Code Unit で表現する文字と、ふたつの Code Unit で表現する文字が混在している。</p>
<h2 id="UTF-16-による変換ロジック">UTF-16 による変換ロジック</h2>
<p>Code Point から Code Unit への変換は、定義された所定のロジックで行われる。</p>
<p>まず、<code>U+10000</code>から<code>U+10FFFF</code>の Code Point がサロゲートペアになり、それ以外の Code Point は Code Point がそのまま Code Unit になる。</p>
<p>サロゲートペアの場合、 2 進数表記の Code Point をゼロパディングして 24 桁にする。<br />
そして以下の表の変換ロジックで、ふたつの 16 ビットのビット列に変換する。</p>
<table>
<thead>
<tr>
<th style="text-align:center;"></th>
<th style="text-align:center;"> Code Point </th>
<th style="text-align:center;"> UTF-16 </th>
<th style="text-align:center;"> 備考 </th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center;">ロジック</td>
<td style="text-align:center;"> <code>000uuuuuyyyyyyxxxxxxxxxx</code> </td>
<td style="text-align:center;"> <code>110110wwwwyyyyyy</code> <code>110111xxxxxxxxxx</code> </td>
<td style="text-align:center;"> <code>wwww = uuuuu - 1</code> </td>
</tr>
<tr>
<td style="text-align:center;">U+1f436(🐶)</td>
<td style="text-align:center;"> <code>000000011111010000110110</code> </td>
<td style="text-align:center;"> <code>1101100000111101</code> <code>1101110000110110</code> </td>
<td style="text-align:center;"> <code>0000 = 00001 - 1</code> </td>
</tr>
</tbody>
</table>
<p><code>U+1f436(🐶)</code>の例も合わせて書いておいた。<br />
<code>1f436</code>をビット列(2 進数)で表現すると<code>11111010000110110</code>なので、それをゼロパディングした<code>000000011111010000110110</code>から変換ロジックが始まる。</p>
<p>そして変換を行うと、<code>U+1f436</code>の Code Unit は<code>1101100000111101</code>(<code>d83d</code>)と<code>1101110000110110</code>(<code>dc36</code>)の組み合わせになる。</p>
<p>このロジックを TypeScript で雑に実装すると以下のようになる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> encode <span class="synStatement">=</span> <span class="synStatement">(</span>codePoint: <span class="synType">string</span><span class="synStatement">)</span>: <span class="synIdentifier">[</span><span class="synType">string</span><span class="synIdentifier">]</span> | <span class="synIdentifier">[</span><span class="synType">string</span><span class="synStatement">,</span> <span class="synType">string</span><span class="synIdentifier">]</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> decimalCodePoint <span class="synStatement">=</span> parseInt<span class="synStatement">(</span>codePoint<span class="synStatement">,</span> <span class="synConstant">16</span><span class="synStatement">);</span>
<span class="synType">const</span> isSurrogatePair <span class="synStatement">=</span>
decimalCodePoint <span class="synStatement">>=</span> <span class="synConstant">0x10000</span> <span class="synConstant">&&</span> decimalCodePoint <span class="synStatement"><=</span> <span class="synConstant">0x10ffff</span><span class="synStatement">;</span>
<span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>isSurrogatePair<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synIdentifier">[</span>codePoint<span class="synIdentifier">]</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span>
<span class="synType">const</span> scalar <span class="synStatement">=</span> decimalCodePoint.toString<span class="synStatement">(</span><span class="synConstant">2</span><span class="synStatement">)</span>.padStart<span class="synStatement">(</span><span class="synConstant">24</span><span class="synStatement">,</span> <span class="synConstant">"0"</span><span class="synStatement">);</span>
<span class="synType">const</span> u <span class="synStatement">=</span> scalar.substring<span class="synStatement">(</span><span class="synConstant">3</span><span class="synStatement">,</span> <span class="synConstant">8</span><span class="synStatement">);</span>
<span class="synType">const</span> x1 <span class="synStatement">=</span> scalar.substring<span class="synStatement">(</span><span class="synConstant">8</span><span class="synStatement">,</span> <span class="synConstant">14</span><span class="synStatement">);</span>
<span class="synType">const</span> x2 <span class="synStatement">=</span> scalar.substring<span class="synStatement">(</span>scalar.length - <span class="synConstant">10</span><span class="synStatement">);</span>
<span class="synType">const</span> w <span class="synStatement">=</span> <span class="synStatement">(</span>parseInt<span class="synStatement">(</span>u<span class="synStatement">,</span> <span class="synConstant">2</span><span class="synStatement">)</span> - <span class="synConstant">1</span><span class="synStatement">)</span>.toString<span class="synStatement">(</span><span class="synConstant">2</span><span class="synStatement">)</span>.padStart<span class="synStatement">(</span><span class="synConstant">4</span><span class="synStatement">,</span> <span class="synConstant">"0"</span><span class="synStatement">);</span>
<span class="synStatement">return</span> <span class="synIdentifier">[</span>
parseInt<span class="synStatement">(</span><span class="synConstant">`110110</span><span class="synSpecial">${</span>w<span class="synSpecial">}${</span>x1<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">,</span> <span class="synConstant">2</span><span class="synStatement">)</span>.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">),</span>
parseInt<span class="synStatement">(</span><span class="synConstant">`110111</span><span class="synSpecial">${</span>x2<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">,</span> <span class="synConstant">2</span><span class="synStatement">)</span>.toString<span class="synStatement">(</span><span class="synConstant">16</span><span class="synStatement">),</span>
<span class="synIdentifier">]</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>encode<span class="synStatement">(</span><span class="synConstant">"0041"</span><span class="synStatement">));</span> <span class="synComment">// [ "0041" ]</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>encode<span class="synStatement">(</span><span class="synConstant">"3042"</span><span class="synStatement">));</span> <span class="synComment">// [ "3042" ]</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>encode<span class="synStatement">(</span><span class="synConstant">"1f436"</span><span class="synStatement">));</span> <span class="synComment">// [ "d83d", "dc36" ]</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
<span class="synSpecial">String</span>.fromCharCode<span class="synStatement">(</span>
...encode<span class="synStatement">(</span><span class="synConstant">"1f436"</span><span class="synStatement">)</span>.map<span class="synStatement">((</span>codeUnit<span class="synStatement">)</span> <span class="synStatement">=></span> parseInt<span class="synStatement">(</span>codeUnit<span class="synStatement">,</span> <span class="synConstant">16</span><span class="synStatement">))</span>
<span class="synStatement">)</span>
<span class="synStatement">);</span> <span class="synComment">// 🐶</span>
</pre>
<h2 id="文字列リテラルとバイト列の相互変換">文字列リテラルとバイト列の相互変換</h2>
<p>Web API の機能を使うことで、文字列リテラルとバイト列の相互変換を行える。</p>
<p>文字列リテラルからバイト列への変換には<code>TextEncoder</code>を使う。<br />
<code>TextEncoder</code>インスタンスの<code>encode</code>メソッドは文字列リテラルを受け取り、それを UTF-8 でエンコードした<code>Uint8Array</code>を返す。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> encoder <span class="synStatement">=</span> <span class="synStatement">new</span> TextEncoder<span class="synStatement">();</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>encoder.encode<span class="synStatement">(</span><span class="synConstant">"A"</span><span class="synStatement">));</span> <span class="synComment">// Uint8Array(1) [ 65 ]</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>encoder.encode<span class="synStatement">(</span><span class="synConstant">"あ"</span><span class="synStatement">));</span> <span class="synComment">// Uint8Array(3) [ 227, 129, 130 ]</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>encoder.encode<span class="synStatement">(</span><span class="synConstant">"🐶"</span><span class="synStatement">));</span> <span class="synComment">// Uint8Array(4) [ 240, 159, 144, 182 ]</span>
</pre>
<p>UTF-8 では Code Unit を符号なし 8 ビット整数で表現し、ひとつの文字を 1 ~ 4 つの Code Unit で表現する。<br />
そのため、<code>あ</code>や<code>🐶</code>のケースを見れば分かるように、 UTF-16 による表現とは一致しないので注意する。</p>
<p>バイト列から文字列リテラルへの変換は<code>TextDecoder</code>で行える。<br />
コンストラクタの引数にはエンコーディング形式を渡すことができ、省略した場合は<code>utf-8</code>になる。</p>
<p><code>utf-8</code>を指定した<code>TextDecoder</code>インスタンスの<code>decode</code>メソッドに、 UTF-8 でエンコードされた<code>Uint8Array</code>を渡すと、デコードした文字列リテラルが返ってくる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> decoder <span class="synStatement">=</span> <span class="synStatement">new</span> TextDecoder<span class="synStatement">(</span><span class="synConstant">"utf-8"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>decoder.decode<span class="synStatement">(new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span><span class="synIdentifier">[</span><span class="synConstant">65</span><span class="synIdentifier">]</span><span class="synStatement">)));</span> <span class="synComment">// A</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>decoder.decode<span class="synStatement">(new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span><span class="synIdentifier">[</span><span class="synConstant">227</span><span class="synStatement">,</span> <span class="synConstant">129</span><span class="synStatement">,</span> <span class="synConstant">130</span><span class="synIdentifier">]</span><span class="synStatement">)));</span> <span class="synComment">// あ</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>decoder.decode<span class="synStatement">(new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span><span class="synIdentifier">[</span><span class="synConstant">240</span><span class="synStatement">,</span> <span class="synConstant">159</span><span class="synStatement">,</span> <span class="synConstant">144</span><span class="synStatement">,</span> <span class="synConstant">182</span><span class="synIdentifier">]</span><span class="synStatement">)));</span> <span class="synComment">// 🐶</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>
decoder.decode<span class="synStatement">(new</span> <span class="synSpecial">Uint8Array</span><span class="synStatement">(</span><span class="synIdentifier">[</span><span class="synConstant">65</span><span class="synStatement">,</span> <span class="synConstant">227</span><span class="synStatement">,</span> <span class="synConstant">129</span><span class="synStatement">,</span> <span class="synConstant">130</span><span class="synStatement">,</span> <span class="synConstant">240</span><span class="synStatement">,</span> <span class="synConstant">159</span><span class="synStatement">,</span> <span class="synConstant">144</span><span class="synStatement">,</span> <span class="synConstant">182</span><span class="synIdentifier">]</span><span class="synStatement">))</span>
<span class="synStatement">);</span> <span class="synComment">// Aあ🐶</span>
</pre>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://ja.wikipedia.org/wiki/UTF-16#%E7%AC%A6%E5%8F%B7%E5%8C%96">UTF-16 - Wikipedia</a></li>
</ul>
numb_86
継続渡しスタイルを使ってプログラムの見通しをよくする
hatenablog://entry/4207112889908490685
2022-08-14T00:48:51+09:00
2022-08-14T00:48:51+09:00 この記事では、継続渡しスタイル(continuation passing style、以下 CPS)の概要と、CPS の活用例を書いていく。 この記事に出てくるコードの動作確認は TypeScript の4.7.4で行っている。 後続の処理を引数として渡す 関数が終わった後に実行される後続の処理をその関数の引数として渡すスタイル、そういったプログラムの書き方を、 CPS と呼ぶ。 例えば、以下のようなコードがあるとする。 const getLength = (str: string): number => str.length; const n: number = getLength("hel…
<p>この記事では、継続渡しスタイル(continuation passing style、以下 CPS)の概要と、CPS の活用例を書いていく。</p>
<p>この記事に出てくるコードの動作確認は TypeScript の<code>4.7.4</code>で行っている。</p>
<h2 id="後続の処理を引数として渡す">後続の処理を引数として渡す</h2>
<p>関数が終わった後に実行される後続の処理をその関数の引数として渡すスタイル、そういったプログラムの書き方を、 CPS と呼ぶ。</p>
<p>例えば、以下のようなコードがあるとする。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> getLength <span class="synStatement">=</span> <span class="synStatement">(</span>str: <span class="synType">string</span><span class="synStatement">)</span>: <span class="synType">number</span> <span class="synStatement">=></span> str.length<span class="synStatement">;</span>
<span class="synType">const</span> n: <span class="synType">number</span> <span class="synStatement">=</span> getLength<span class="synStatement">(</span><span class="synConstant">"hello"</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>n<span class="synStatement">);</span> <span class="synComment">// 5</span>
</pre>
<p><code>getLength("hello")</code>の結果を<code>n</code>に代入し、それを使って<code>console.log</code>を実行している。</p>
<p><code>getLength</code>を CPS に書き換えると次のようになる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> getLengthCps <span class="synStatement">=</span> <span class="synStatement"><</span>T<span class="synStatement">>(</span>cont: <span class="synStatement">(</span>x: <span class="synType">number</span><span class="synStatement">)</span> <span class="synStatement">=></span> T<span class="synStatement">,</span> str: <span class="synType">string</span><span class="synStatement">)</span>: T <span class="synStatement">=></span>
cont<span class="synStatement">(</span>str.length<span class="synStatement">);</span>
</pre>
<p><code>getLength</code>は<code>str.length</code>を返していたが、<code>getLengthCps</code>は<code>str.length</code>を「関数が終わった後に実行される後続の処理」である<code>cont</code>に渡している。</p>
<p><code>number</code>を受け取る関数ならどんなものでも、<code>cont</code>として渡すことができる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> getLengthCps <span class="synStatement">=</span> <span class="synStatement"><</span>T<span class="synStatement">>(</span>cont: <span class="synStatement">(</span>x: <span class="synType">number</span><span class="synStatement">)</span> <span class="synStatement">=></span> T<span class="synStatement">,</span> str: <span class="synType">string</span><span class="synStatement">)</span>: T <span class="synStatement">=></span>
cont<span class="synStatement">(</span>str.length<span class="synStatement">);</span>
getLengthCps<span class="synStatement">(</span><span class="synSpecial">console</span>.log<span class="synStatement">,</span> <span class="synConstant">"hello"</span><span class="synStatement">);</span> <span class="synComment">// 5</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>getLengthCps<span class="synStatement">((</span>length<span class="synStatement">)</span> <span class="synStatement">=></span> length * <span class="synConstant">3</span><span class="synStatement">,</span> <span class="synConstant">"foo"</span><span class="synStatement">));</span> <span class="synComment">// 9</span>
</pre>
<h2 id="CPS-を使ったリファクタリング">CPS を使ったリファクタリング</h2>
<p>CPS の利用例のひとつとして、特定の条件を満たしたときにのみ後続の処理を実行する、というプログラムを書いてみる。</p>
<p>CPS を使っていない、以下のコードがあったとする。<br />
少し長いが、<code>getEmployeesPageProps</code>と<code>getOfficesPageProps</code>の概要さえ理解できれば問題ない。<br />
これらの関数の返り値をコンポーネントに渡して View を作ることを想定している。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> Employees <span class="synStatement">=</span> <span class="synType">string</span><span class="synIdentifier">[]</span><span class="synStatement">;</span>
<span class="synStatement">type</span> Offices <span class="synStatement">=</span> <span class="synType">string</span><span class="synIdentifier">[]</span><span class="synStatement">;</span>
<span class="synStatement">type</span> GetPageProps<span class="synStatement"><</span>T<span class="synStatement">></span> <span class="synStatement">=</span> <span class="synStatement">(</span>
sessionId: <span class="synType">string</span>
<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span> ok: <span class="synConstant">false</span><span class="synStatement">;</span> message: <span class="synType">string</span> <span class="synIdentifier">}</span> | <span class="synIdentifier">{</span> ok: <span class="synConstant">true</span><span class="synStatement">;</span> data: T <span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> CORRECT_SESSION_ID <span class="synStatement">=</span> <span class="synConstant">"123"</span><span class="synStatement">;</span>
<span class="synType">const</span> auth <span class="synStatement">=</span> <span class="synStatement">(</span>
sessionId: <span class="synType">string</span>
<span class="synStatement">)</span>: <span class="synIdentifier">{</span> ok: <span class="synConstant">true</span><span class="synStatement">;</span> companyId: <span class="synType">string</span> <span class="synIdentifier">}</span> | <span class="synIdentifier">{</span> ok: <span class="synConstant">false</span> <span class="synIdentifier">}</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>sessionId <span class="synStatement">===</span> CORRECT_SESSION_ID<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
ok: <span class="synConstant">true</span><span class="synStatement">,</span>
companyId: <span class="synConstant">"1"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
ok: <span class="synConstant">false</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> getEmployeesPageProps: GetPageProps<span class="synStatement"><</span><span class="synIdentifier">{</span> employees: Employees | <span class="synType">null</span> <span class="synIdentifier">}</span><span class="synStatement">></span> <span class="synStatement">=</span> <span class="synStatement">(</span>
sessionId
<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> authResult <span class="synStatement">=</span> auth<span class="synStatement">(</span>sessionId<span class="synStatement">);</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>authResult.ok <span class="synStatement">===</span> <span class="synConstant">false</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
ok: <span class="synConstant">false</span><span class="synStatement">,</span>
message: <span class="synConstant">"Unauthorized"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span>
<span class="synType">const</span> dummyDb <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Map</span><span class="synStatement">(</span><span class="synIdentifier">[[</span><span class="synConstant">"1"</span><span class="synStatement">,</span> <span class="synIdentifier">[</span><span class="synConstant">"Alice"</span><span class="synStatement">,</span> <span class="synConstant">"Bob"</span><span class="synIdentifier">]]]</span><span class="synStatement">);</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
ok: <span class="synConstant">true</span><span class="synStatement">,</span>
data: <span class="synIdentifier">{</span> employees: dummyDb.<span class="synStatement">get(</span>authResult.companyId<span class="synStatement">)</span> ?? <span class="synType">null</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> getOfficesPageProps: GetPageProps<span class="synStatement"><</span><span class="synIdentifier">{</span> offices: Offices | <span class="synType">null</span> <span class="synIdentifier">}</span><span class="synStatement">></span> <span class="synStatement">=</span> <span class="synStatement">(</span>
sessionId
<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> authResult <span class="synStatement">=</span> auth<span class="synStatement">(</span>sessionId<span class="synStatement">);</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>authResult.ok <span class="synStatement">===</span> <span class="synConstant">false</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
ok: <span class="synConstant">false</span><span class="synStatement">,</span>
message: <span class="synConstant">"Unauthorized"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span>
<span class="synType">const</span> dummyDb <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Map</span><span class="synStatement">(</span><span class="synIdentifier">[[</span><span class="synConstant">"1"</span><span class="synStatement">,</span> <span class="synIdentifier">[</span><span class="synConstant">"London"</span><span class="synStatement">,</span> <span class="synConstant">"Paris"</span><span class="synIdentifier">]]]</span><span class="synStatement">);</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
ok: <span class="synConstant">true</span><span class="synStatement">,</span>
data: <span class="synIdentifier">{</span> offices: dummyDb.<span class="synStatement">get(</span>authResult.companyId<span class="synStatement">)</span> ?? <span class="synType">null</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>getEmployeesPageProps<span class="synStatement">(</span><span class="synConstant">"xyz"</span><span class="synStatement">));</span> <span class="synComment">// { ok: false, message: 'Unauthorized' }</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>getEmployeesPageProps<span class="synStatement">(</span><span class="synConstant">"123"</span><span class="synStatement">));</span> <span class="synComment">// { ok: true, data: { employees: [ 'Alice', 'Bob' ] } }</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>getOfficesPageProps<span class="synStatement">(</span><span class="synConstant">"xyz"</span><span class="synStatement">));</span> <span class="synComment">// { ok: false, message: 'Unauthorized' }</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>getOfficesPageProps<span class="synStatement">(</span><span class="synConstant">"123"</span><span class="synStatement">));</span> <span class="synComment">// { ok: true, data: { offices: [ 'London', 'Paris' ] } }</span>
</pre>
<p><code>getEmployeesPageProps</code>と<code>getOfficesPageProps</code>はどちらも、以下の処理を行っている。</p>
<ol>
<li>引数として渡された<code>sessionId</code>を使って認証を行う</li>
<li>認証に失敗した場合はその旨を返し、処理を終了する</li>
<li>認証に成功した場合は手に入れた<code>companyId</code>を使って<code>Employees</code>もしくは<code>Offices</code>を取得し、それを含んだ<code>GetPageProps</code>を返す</li>
</ol>
<p>このうち、<code>1</code>と<code>2</code>は全く同じコードなので、これを共通化したい。<br />
CPS を使って「認証が成功した後の処理を引数として渡す」という書き方にすることで、これを実現できる。</p>
<p>以下の<code>continueWithAuth</code>は「認証が成功した後の処理」を<code>cont</code>として受け取り、認証が成功したときにのみ<code>cont</code>を呼び出している。<br />
こうすることで、<code>1</code>と<code>2</code>の処理を共通化し、<code>3</code>として任意の処理を渡せるようになる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> continueWithAuth <span class="synStatement">=</span> <span class="synStatement"><</span>T<span class="synStatement">>(</span>
cont: <span class="synStatement">(</span>companyId: <span class="synType">string</span><span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span> ok: <span class="synConstant">true</span><span class="synStatement">;</span> data: T <span class="synIdentifier">}</span><span class="synStatement">,</span>
sessionId: <span class="synType">string</span>
<span class="synStatement">)</span>: ReturnType<span class="synStatement"><</span>GetPageProps<span class="synStatement"><</span>T<span class="synStatement">>></span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> authResult <span class="synStatement">=</span> auth<span class="synStatement">(</span>sessionId<span class="synStatement">);</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>authResult.ok <span class="synStatement">===</span> <span class="synConstant">false</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
ok: <span class="synConstant">false</span><span class="synStatement">,</span>
message: <span class="synConstant">"Unauthorized"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">return</span> cont<span class="synStatement">(</span>authResult.companyId<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> getEmployeesPageProps: GetPageProps<span class="synStatement"><</span><span class="synIdentifier">{</span> employees: Employees | <span class="synType">null</span> <span class="synIdentifier">}</span><span class="synStatement">></span> <span class="synStatement">=</span> <span class="synStatement">(</span>
sessionId
<span class="synStatement">)</span> <span class="synStatement">=></span>
continueWithAuth<span class="synStatement">((</span>companyId<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> dummyDb <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Map</span><span class="synStatement">(</span><span class="synIdentifier">[[</span><span class="synConstant">"1"</span><span class="synStatement">,</span> <span class="synIdentifier">[</span><span class="synConstant">"Alice"</span><span class="synStatement">,</span> <span class="synConstant">"Bob"</span><span class="synIdentifier">]]]</span><span class="synStatement">);</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
ok: <span class="synConstant">true</span><span class="synStatement">,</span>
data: <span class="synIdentifier">{</span>
employees: dummyDb.<span class="synStatement">get(</span>companyId<span class="synStatement">)</span> ?? <span class="synType">null</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span> sessionId<span class="synStatement">);</span>
<span class="synType">const</span> getOfficesPageProps: GetPageProps<span class="synStatement"><</span><span class="synIdentifier">{</span> offices: Offices | <span class="synType">null</span> <span class="synIdentifier">}</span><span class="synStatement">></span> <span class="synStatement">=</span> <span class="synStatement">(</span>
sessionId
<span class="synStatement">)</span> <span class="synStatement">=></span>
continueWithAuth<span class="synStatement">((</span>companyId<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> dummyDb <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Map</span><span class="synStatement">(</span><span class="synIdentifier">[[</span><span class="synConstant">"1"</span><span class="synStatement">,</span> <span class="synIdentifier">[</span><span class="synConstant">"London"</span><span class="synStatement">,</span> <span class="synConstant">"Paris"</span><span class="synIdentifier">]]]</span><span class="synStatement">);</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
ok: <span class="synConstant">true</span><span class="synStatement">,</span>
data: <span class="synIdentifier">{</span>
offices: dummyDb.<span class="synStatement">get(</span>companyId<span class="synStatement">)</span> ?? <span class="synType">null</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span> sessionId<span class="synStatement">);</span>
</pre>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://m-hiyama.hatenablog.com/entry/20080116/1200468797">CPS(継続渡し方式)変換をJavaScriptで説明してみるべ、ナーニ、たいしたことねーべよ - 檜山正幸のキマイラ飼育記 (はてなBlog)</a></li>
</ul>
numb_86
Node.js Stream の初歩
hatenablog://entry/4207112889897451169
2022-07-09T14:44:22+09:00
2022-07-09T14:44:22+09:00 Node.js には Stream というインターフェイスが用意されており、これを使うことでデータをストリーミングできる。 Stream を使うことで、データの全てをメモリに保持するのではなく、少しずつ順番にデータを処理していくことが可能になる。 この記事では、Stream の基本的な使い方について説明していく。 WHATWG で定義している Stream はまた別の概念なので、注意する。この記事で扱っている Stream は、それとは別に以前から Node.js に実装されている Stream である。 以下の環境で動作確認している。 Node.js のバージョン 16.15.1 使っている…
<p>Node.js には Stream というインターフェイスが用意されており、これを使うことでデータをストリーミングできる。<br />
Stream を使うことで、データの全てをメモリに保持するのではなく、少しずつ順番にデータを処理していくことが可能になる。</p>
<p>この記事では、Stream の基本的な使い方について説明していく。</p>
<p>WHATWG で定義している Stream はまた別の概念なので、注意する。この記事で扱っている Stream は、それとは別に以前から Node.js に実装されている Stream である。</p>
<p>以下の環境で動作確認している。</p>
<ul>
<li>Node.js のバージョン
<ul>
<li>16.15.1</li>
</ul>
</li>
<li>使っている npm ライブラリ
<ul>
<li>@types/node@16.11.43</li>
<li>ts-node-dev@2.0.0</li>
<li>typescript@4.7.4</li>
</ul>
</li>
</ul>
<h2>環境構築</h2>
<p>まず最初に、手元で実際にコードを動かすための環境を構築する。<br />
TypeScript で書くための準備作業なので、JavaScript で書く場合はこの工程は不要。</p>
<p>適当なディレクトリを作り、以下のコマンドを実行する。</p>
<pre class="code" data-lang="" data-unlink>% yarn init -y
% yarn add -D typescript ts-node-dev @types/node@16
% touch index.ts</pre>
<p>最後に、以下の内容の<code>tsconfig.json</code>を作る。</p>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">compilerOptions</span>": <span class="synSpecial">{</span>
"<span class="synStatement">strict</span>": <span class="synConstant">true</span>,
"<span class="synStatement">lib</span>": <span class="synSpecial">[</span>"<span class="synConstant">esnext</span>", "<span class="synConstant">DOM</span>"<span class="synSpecial">]</span>,
"<span class="synStatement">module</span>": "<span class="synConstant">NodeNext</span>",
"<span class="synStatement">target</span>": "<span class="synConstant">ESNext</span>"
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p>この状態で<code>yarn ts-node-dev --respawn index.ts</code>を実行すると、<code>index.ts</code>に書いた内容を実行してくれるようになる。</p>
<h2>Stream はインターフェイス</h2>
<p>Stream はインターフェイスであり、そのインターフェイスを満たしているものなら何であれ Stream として扱うことができる。<br />
自分で Stream を実装してもいいが、予め Node.js が用意しているものを利用することが多い。<br />
例えば、<code>fs</code>モジュールはファイルを Stream として扱う仕組みを用意しており、これを使うことでファイルの読み書きをストリーミングで行うことができる。</p>
<p>データを読み込むための Stream である Readable Stream や、書き込むための Stream である Writable Stream など、Stream にはいくつかの種類があり、目的に応じて使い分ける。<br />
先程のファイルの例でいえば、ファイルの読み込みは Readable Stream を使って行い、書き込みは Writable Stream を使って行う。</p>
<p>早速、Stream でファイルを読み込んでみる。あくまでも雰囲気を掴むためのものなので、この時点でコードの内容を理解する必要はない。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> Readable <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"stream"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs: Readable <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">8</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">for</span> <span class="synStatement">await</span> <span class="synStatement">(</span><span class="synType">const</span> chunk <span class="synStatement">of</span> rs<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>chunk<span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"\n==delimit==\n"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
main<span class="synStatement">();</span>
</pre>
<p>上記のコードを実行すると、以下のログが流れるはず。</p>
<pre class="code" data-lang="" data-unlink>import {
==delimit==
createR
==delimit==
eadStrea
==delimit==
// 以下省略</pre>
<p><code>__filename</code>は<code>index.ts</code>自身を指しているので、このファイルの内容を<code>8</code>バイトずつ読み込み、その内容を<code>console.log</code>に渡している。<br />
このように、データを少しずつ扱う、ということが Stream によって可能になる。</p>
<h2>Readable Stream</h2>
<p>まずは、<code>fs.createReadStream</code>を題材にして、Readable Stream の基本的な使い方を見ていく。</p>
<p><code>createReadStream</code>にファイルのパスを渡すことで、Readable Stream を作ることができる。<br />
第二引数はオプションで、ここで<code>encoding</code>を指定すると、読み込んだデータをその形式でデコードするようになる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> Readable <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"stream"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>rs <span class="synStatement">instanceof</span> Readable<span class="synStatement">);</span> <span class="synComment">// true</span>
</pre>
<p>Readable Stream の<code>data</code>イベントにリスナーを設定すると、そのリスナーにデータが渡される。<br />
そして全てのデータが消費されると、<code>end</code>イベントが発生する。<br />
そのため下記の例では、このコードの内容と<code>end!</code>がログに流れる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> Readable <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"stream"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.on<span class="synStatement">(</span><span class="synConstant">"data"</span><span class="synStatement">,</span> <span class="synStatement">(</span>chunk<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>chunk<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.on<span class="synStatement">(</span><span class="synConstant">"end"</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"end!"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<p>また、Readable Stream は AsyncIterable でもあるので、<code>for await...of</code>構文を使ってデータを取り出すこともできる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs: AsyncIterable<span class="synStatement"><</span><span class="synType">string</span><span class="synStatement">></span> <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> receive<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">for</span> <span class="synStatement">await</span> <span class="synStatement">(</span><span class="synType">const</span> chunk <span class="synStatement">of</span> rs<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>chunk<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"end!"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
receive<span class="synStatement">();</span>
</pre>
<p>消費されたデータは消えてしまうので、下記の例では<code>receive</code>を 3 回実行しているがデータの読み取りは 1 回しか発生しない。<br />
そのため、ファイルの内容が表示されたあと、<code>end!</code>が 3 つ並んで表示される。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs: AsyncIterable<span class="synStatement"><</span><span class="synType">string</span><span class="synStatement">></span> <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> receive<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">for</span> <span class="synStatement">await</span> <span class="synStatement">(</span><span class="synType">const</span> chunk <span class="synStatement">of</span> rs<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>chunk<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"end!"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
receive<span class="synStatement">();</span>
receive<span class="synStatement">();</span>
receive<span class="synStatement">();</span>
</pre>
<h2>highWaterMark</h2>
<p>Stream は、その内部にバッファとしてデータを保持する。そして各 Stream は、バッファにどの程度のデータまで保持するのかを示す、highWaterMark という名前の閾値を持っている。<br />
この閾値を過度に超えないように上手く制御することで、「データを少しずつ処理する」ことが可能になる。</p>
<p>Readable Stream の場合、<code>readableHighWaterMark</code>に highWaterMark の値が格納されている。<br />
createReadStream で作られた Readable Stream の場合、デフォルト値は<code>65536</code>バイト(<code>64 * 1024 = 65536</code>なので<code>64</code>キロバイト)だが、<code>highWaterMark</code>オプションで設定することもできる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs1 <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">);</span>
<span class="synType">const</span> rs2 <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> highWaterMark: <span class="synConstant">16</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>rs1.readableHighWaterMark<span class="synStatement">);</span> <span class="synComment">// 65536</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>rs2.readableHighWaterMark<span class="synStatement">);</span> <span class="synComment">// 16</span>
</pre>
<p>下記の例だと highWaterMark を<code>16</code>に設定しているので、まず<code>16</code>バイト分のデータだけ内部バッファに格納し、それを<code>console.log</code>に渡す。<br />
そしてそのデータは捨てられ、また次の<code>16</code>バイトが読み込まれる。そのため、内部バッファには常に<code>16</code>バイト以下のデータしか格納されないようになっている。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">16</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> receive<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">for</span> <span class="synStatement">await</span> <span class="synStatement">(</span><span class="synType">const</span> chunk <span class="synStatement">of</span> rs<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>chunk<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
receive<span class="synStatement">();</span>
</pre>
<h2>Writable Stream</h2>
<p>データの書き込みには、Writable Stream を使う。</p>
<p>ファイルの場合は<code>createWriteStream</code>で Writable Stream を作れる。<br />
以下のように書くと、<code>dest.txt</code>というファイルへの書き込みを行う Writable Stream が作られる。<br />
<code>createWriteStream</code>の場合、<code>encoding</code>はデフォルトで<code>utf8</code>なので、この記事では特に指定していない。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> Writable <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"stream"</span><span class="synStatement">;</span>
<span class="synType">const</span> ws: Writable <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">);</span>
</pre>
<p>Readable Stream は<code>write</code>メソッドを持っており、それを使ってデータの書き込みができる。<br />
書き込みが終わったことを Writeable Stream に伝えるには<code>end</code>メソッドを使う。そして書き込みが終わったことを知った Writable Stream は、<code>finish</code>イベントを発火する。</p>
<p>以下のコードの実行すると、このコードの内容を<code>16</code>バイト毎に改行しながら<code>dest.txt</code>に書き込んでいき、それが完了するとログに<code>finish!</code>が流れる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream<span class="synStatement">,</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">16</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> ws <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">);</span>
ws.on<span class="synStatement">(</span><span class="synConstant">"finish"</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"finish!"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.on<span class="synStatement">(</span><span class="synConstant">"data"</span><span class="synStatement">,</span> <span class="synStatement">(</span>chunk<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
ws.write<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">${</span>chunk<span class="synSpecial">}</span><span class="synConstant">\n`</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<h2>highWaterMark を頼りに「水位」を調節する</h2>
<p>Writable Stream も内部バッファを持っており、そこにデータが蓄積されていく。<br />
そしてやはり highWaterMark を設定することができる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> ws1 <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">);</span>
<span class="synType">const</span> ws2 <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> highWaterMark: <span class="synConstant">8</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>ws1.writableHighWaterMark<span class="synStatement">);</span> <span class="synComment">// 16384</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>ws2.writableHighWaterMark<span class="synStatement">);</span> <span class="synComment">// 8</span>
</pre>
<p>そしてこの値に応じて内部バッファに格納されるデータ量が自動的に調節される、というわけではない。<br />
highWaterMark はただ閾値を表現しているだけであり、それを超えないことを保証するようなものではない。</p>
<p>ではどうすればいいのかというと、データ量を監視しながら上手く調節する必要がある。<br />
データ量の状況を知るための手段がいくつか用意されているので、それを見ながら、手動で調節を行う。</p>
<p>まずは、状況を知るための手段について。<br />
「<code>write</code>メソッドの返り値」と「<code>drain</code>イベント」を利用することで、highWaterMark を超えているか確認できる。</p>
<p><code>write</code>メソッドは、内部バッファに格納されているデータ量が highWaterMark を超えていなければ<code>true</code>を、超えてしまっていれば<code>false</code>を返す。</p>
<p>以降のコードの実行結果は、筆者の手元の環境によるもの。<br />
データの処理速度は常に一定というわけではないので、結果が変わることもある。</p>
<p>以下のコードだと 5 回書き込みが行われるが、<code>write</code>メソッドはいずれも<code>true</code>を返す。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream<span class="synStatement">,</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">64</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> ws <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
highWaterMark: <span class="synConstant">256</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.on<span class="synStatement">(</span><span class="synConstant">"data"</span><span class="synStatement">,</span> <span class="synStatement">(</span>chunk<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>ws.write<span class="synStatement">(</span>chunk<span class="synStatement">));</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<p>同じ内容で Writable Stream の highWaterMark だけ<code>16</code>に変更する。<br />
そうすると、5 回とも<code>false</code>を返す。highWaterMark を小さくしたことで、内部バッファに格納されているデータ量が常にそれを超えるようになってしまった。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream<span class="synStatement">,</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">64</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> ws <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
highWaterMark: <span class="synConstant">16</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.on<span class="synStatement">(</span><span class="synConstant">"data"</span><span class="synStatement">,</span> <span class="synStatement">(</span>chunk<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>ws.write<span class="synStatement">(</span>chunk<span class="synStatement">));</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<p><code>drain</code>イベントは、highWaterMark を超えてしまったあと、内部バッファに格納されている全てのデータが排出されたときに発生する。<br />
Node.js Stream では、drain や highWaterMark のように、「水」に関係した単語でデータ量の状況を表現している。</p>
<p>下記のコードだと、<code>drain!</code>がログに流れる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream<span class="synStatement">,</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">64</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> ws <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
highWaterMark: <span class="synConstant">16</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
ws.on<span class="synStatement">(</span><span class="synConstant">"drain"</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"drain!"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.on<span class="synStatement">(</span><span class="synConstant">"data"</span><span class="synStatement">,</span> <span class="synStatement">(</span>chunk<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
ws.write<span class="synStatement">(</span>chunk<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<p>だが以下のようにすると、そもそも一度も highWaterMark を超えないので、<code>drain</code>イベントも発生しないままプログラムが終了する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream<span class="synStatement">,</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">64</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> ws <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
highWaterMark: <span class="synConstant">256</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
ws.on<span class="synStatement">(</span><span class="synConstant">"drain"</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"drain!"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.on<span class="synStatement">(</span><span class="synConstant">"data"</span><span class="synStatement">,</span> <span class="synStatement">(</span>chunk<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
ws.write<span class="synStatement">(</span>chunk<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<p>これら 2 つの指標を使い、流れ込んでくるデータ量を制御することで、内部バッファに格納されているデータ量が highWaterMark を過度に超えないようにすることができる。<br />
具体的には、<code>write</code>が<code>false</code>を返したらデータの読み込みを一時停止し、<code>drain</code>が発生したら再開する。</p>
<p>それを実装したのが、以下のコード。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream<span class="synStatement">,</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">64</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> ws <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
highWaterMark: <span class="synConstant">16</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
ws.on<span class="synStatement">(</span><span class="synConstant">"drain"</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"drain!"</span><span class="synStatement">);</span>
rs.resume<span class="synStatement">();</span> <span class="synComment">// 読み込みを再開する</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.on<span class="synStatement">(</span><span class="synConstant">"data"</span><span class="synStatement">,</span> <span class="synStatement">(</span>chunk<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> ok <span class="synStatement">=</span> ws.write<span class="synStatement">(</span>chunk<span class="synStatement">);</span>
<span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>ok<span class="synStatement">)</span> <span class="synIdentifier">{</span>
rs.pause<span class="synStatement">();</span> <span class="synComment">// 読み込みを一時停止する</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<p>このコードを実行すると<code>drain!</code>が何度もログに流れる。<br />
このことから、「highWaterMark を超えたのでデータの読み込みを一時停止する → drain イベントが発生したので再開する → 再び highWaterMark を超える」というサイクルが何度も繰り返されていることが分かる。</p>
<h2>pipe による自動調節</h2>
<p>Readable Stream は<code>pipe</code>というメソッドを持っており、引数に Writable Stream を渡すことで、読み込んだデータを Writable Stream に流し込むことができる。<br />
このメソッドを使えば、<code>write</code>メソッドや<code>data</code>イベントを使わずにデータを書き込める。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream<span class="synStatement">,</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">64</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> ws <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
highWaterMark: <span class="synConstant">16</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.pipe<span class="synStatement">(</span>ws<span class="synStatement">);</span>
</pre>
<p>さらに<code>pipe</code>メソッドは、データ量の調節も自動的に行なってくれる。<br />
そのため実は、<code>pipe</code>メソッドを使えば、先程書いたような調節処理を自分で実装する必要はない。</p>
<p>下記のコードを実行すると<code>pause!</code>が複数回表示されるので、<code>pipe</code>メソッドを実行すると内部的に<code>pause</code>メソッドが呼ばれていることが分かる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> createReadStream<span class="synStatement">,</span> createWriteStream <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synType">const</span> rs <span class="synStatement">=</span> createReadStream<span class="synStatement">(</span><span class="synSpecial">__filename</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
encoding: <span class="synConstant">"utf-8"</span><span class="synStatement">,</span>
highWaterMark: <span class="synConstant">64</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> ws <span class="synStatement">=</span> createWriteStream<span class="synStatement">(</span><span class="synConstant">"dest.txt"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
highWaterMark: <span class="synConstant">16</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.on<span class="synStatement">(</span><span class="synConstant">"pause"</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">"pause!"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
rs.pipe<span class="synStatement">(</span>ws<span class="synStatement">);</span>
</pre>
<h2>参考資料</h2>
<ul>
<li><a href="https://nodejs.org/docs/latest-v16.x/api/stream.html">Stream | Node.js v16.16.0 Documentation</a></li>
<li><a href="https://techblog.yahoo.co.jp/advent-calendar-2016/node-stream-highwatermark/">highWaterMarkから探るNode.jsのStreamの仕組み - Yahoo! JAPAN Tech Blog</a></li>
</ul>
numb_86
『図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書』を読んだ
hatenablog://entry/13574176438098083966
2022-06-01T22:28:14+09:00
2022-06-01T22:28:14+09:00 Amazon Web Services(以下 AWS)の入門書。 AWS やその前提となる知識について、非常に平易に解説している。理解を促すための図も豊富で、分かりやすい。 AWS を学ぶ最初の一冊としてオススメ。 gihyo.jp AWS が提供しているサービスは多岐に渡り、独自の用語も多い。 そのため全体像を掴みづらく、入門者の心をへし折りやすい技術だと思う。少なくとも自分は何度もへし折られてきた。 できることが膨大すぎて、どこから手を付けていいのか分からない。 調べ物をしていても、次から次へと知らない用語が出てきて途方に暮れる。 結果、ググって出てきた記事の内容を模倣してお茶を濁す。取り…
<p>Amazon Web Services(以下 AWS)の入門書。<br />
AWS やその前提となる知識について、非常に平易に解説している。理解を促すための図も豊富で、分かりやすい。<br />
AWS を学ぶ最初の一冊としてオススメ。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgihyo.jp%2Fbook%2F2019%2F978-4-297-10889-2" title="図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://gihyo.jp/book/2019/978-4-297-10889-2">gihyo.jp</a></cite></p>
<p>AWS が提供しているサービスは多岐に渡り、独自の用語も多い。<br />
そのため全体像を掴みづらく、入門者の心をへし折りやすい技術だと思う。少なくとも自分は何度もへし折られてきた。</p>
<p>できることが膨大すぎて、どこから手を付けていいのか分からない。<br />
調べ物をしていても、次から次へと知らない用語が出てきて途方に暮れる。<br />
結果、ググって出てきた記事の内容を模倣してお茶を濁す。取り敢えずやりたいことは実現できているのだが、自分がどんな操作をしたのかは分かっていない。本当に、「言われた通りにやったら動いた」というだけ。<br />
これではいつまで経っても自分で考え判断できるようにはならないし、いい感じの記事が見つからなければ困ってしまう。<br />
AWS の詳細について知る必要はないと思うが、主要なサービスについては、何のための機能でどういうことが出来るのかは、分かっておいたほうがいい。</p>
<p>最近も、Docker や Kubernetes の勉強をしようとして ECS を使おうとしたが、VPC が全く分からず詰まってしまった。<br />
自分が触ったことがあるのは S3, CloudFront, Lambda あたりなので、VPC とは縁がなかった。</p>
<p>いつもこんな感じだな、もう少しちゃんと AWS を理解しておきたいなと思っていたときに本書を見つけたので、読んでみた。<br />
著書が『仕組みと使い方がわかる Docker&Kubernetesのきほんのきほん』を書いた人で、この本が分かりやすかったので、本書もよさそうに思えた。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2021%2F09%2F25%2F213726" title="『仕組みと使い方がわかる Docker&Kubernetesのきほんのきほん』を読んだ - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://numb86-tech.hatenablog.com/entry/2021/09/25/213726">numb86-tech.hatenablog.com</a></cite></p>
<p>実際、非常によい入門書だった。<br />
ただ単に AWS について解説するのではなく、その前提となる知識についてきちんと説明しているのがよい。<br />
「IP アドレスをドメインに変換する DNS という仕組みがあって」とか、「サブネットとは」とか、かなり初歩の部分から説明している。<br />
そのため、専門知識がない人でも問題なく読める。開発経験がない人でも読めるかもしれない。</p>
<p>一方で、既にある程度 AWS を理解している人にとっては、あまり得るものはないかもしれない。<br />
また、あくまでも概念や用語の解説書なので、具体的な操作方法は一切載っていない。本書を読んだだけでは、実際に AWS で何かを出来るようにはならないと思う。<br />
上述したように自分はまず概念を理解したいと思っていたので本書が合っていたが、とにかく手を動かして覚えたい場合は、他の入門書のほうがよいと思う。</p>
numb_86
Docker の volume と network の初歩
hatenablog://entry/13574176438085999211
2022-04-24T22:12:35+09:00
2022-04-24T22:12:35+09:00 Docker の volume は、コンテナが使うデータを永続化するための仕組みで、これを使うことでコンテナのライフサイクルとは別にデータを管理することができる。 また、network という機能を使うことで、コンテナ間で通信ができるようになる。 この記事では、volume と network の基本的な使い方を見ていく。 コンテナを削除すればそのなかにあるデータも削除されてしまう 基本的に Docker のコンテナは、一度作ったものを長く大切に使い続けるのではなく、作成と破棄を繰り返して使うことが多い。 そしてコンテナを破棄すれば、コンテナのなかにあるデータも当然失われてしまう。 MySQL…
<p>Docker の volume は、コンテナが使うデータを永続化するための仕組みで、これを使うことでコンテナのライフサイクルとは別にデータを管理することができる。<br />
また、network という機能を使うことで、コンテナ間で通信ができるようになる。<br />
この記事では、volume と network の基本的な使い方を見ていく。</p>
<h2>コンテナを削除すればそのなかにあるデータも削除されてしまう</h2>
<p>基本的に Docker のコンテナは、一度作ったものを長く大切に使い続けるのではなく、作成と破棄を繰り返して使うことが多い。<br />
そしてコンテナを破棄すれば、コンテナのなかにあるデータも当然失われてしまう。<br />
MySQL のコンテナで試してみる。</p>
<p>まずコンテナを作り起動させる。</p>
<pre class="code" data-lang="" data-unlink>% docker run --name test_db -dit -e MYSQL_ROOT_PASSWORD=password mysql:8</pre>
<p>以下のコマンドを入力するとパスワードを求められるので、先程<code>MYSQL_ROOT_PASSWORD</code>として設定した<code>password</code>を入力する。</p>
<pre class="code" data-lang="" data-unlink>% docker exec -it test_db mysql -p</pre>
<p>そうすると MySQL に接続できるので、<code>sample_db</code>というデータベースを作ってみる。</p>
<pre class="code" data-lang="" data-unlink>mysql> CREATE DATABASE sample_db;
Query OK, 1 row affected (0.01 sec)
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sample_db |
| sys |
+--------------------+
5 rows in set (0.01 sec)
mysql> exit
Bye</pre>
<p>その後、<code>test_db</code>コンテナを一度停止させてから再度起動してアクセスしても、<code>sample_db</code>は存在する。</p>
<pre class="code" data-lang="" data-unlink>% docker stop test_db
% docker start test_db
% docker exec -it test_db mysql -p</pre>
<pre class="code" data-lang="" data-unlink>mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sample_db |
| sys |
+--------------------+
5 rows in set (0.01 sec)</pre>
<p>だがコンテナを削除し新しく作り直してからその中身を確認すると、<code>sample_db</code>は存在しない。</p>
<pre class="code" data-lang="" data-unlink>% docker stop test_db
% docker rm test_db
% docker run --name test_db -dit -e MYSQL_ROOT_PASSWORD=password mysql:8
% docker exec -it test_db mysql -p</pre>
<pre class="code" data-lang="" data-unlink>mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)</pre>
<p>今見ているコンテナは、名前こそ<code>test_db</code>ではあるが最初に作成したコンテナとは別のコンテナなのだから、<code>sample_db</code>が存在しないのは当然といえる。</p>
<p>このように、コンテナを削除してしまうと、コンテナに対して行った操作やそれによって生まれたデータは、全て失われてしまう。</p>
<h2>volume を使ってデータを永続化する</h2>
<p>Docker が提供する volume という機能を使うことで、データを永続化できる。<br />
volume を使うと、コンテナではなくホストマシンにデータを保存するようになる。そしてそれでいて、コンテナのなかにデータが存在するかのように扱うことができるので、コンテナから簡単にデータを利用することができる。</p>
<p>まず、先ほど作成した<code>test_db</code>はもう使わないので、停止し削除する。</p>
<pre class="code" data-lang="" data-unlink>% docker stop test_db
% docker rm test_db</pre>
<p>volume を使うためにはまず、<code>docker volume create volumeの名前</code>で volume を作る必要がある。<br />
今回は<code>mysql_volume</code>という volume を作ることにする。</p>
<pre class="code" data-lang="" data-unlink>% docker volume create mysql_volume</pre>
<p>そしてコンテナを作る際に、<code>-v ボリューム名:コンテナの記憶領域のパス</code>というオプションをつければいい。</p>
<pre class="code" data-lang="" data-unlink>% docker run --name first_db -dit -e MYSQL_ROOT_PASSWORD=password -v mysql_volume:/var/lib/mysql mysql:8</pre>
<p>上記の例では、<code>-v mysql_volume:/var/lib/mysql</code>というオプションをつけて、<code>first_db</code>という名前のコンテナを作っている。</p>
<p>「コンテナの記憶領域のパス」として<code>/var/lib/mysql</code>を指定しているが、こうすることで、volume に保存されているデータを、コンテナの<code>/var/lib/mysql</code>に存在するかのように扱うことができる。シンボリックリンクのようなもので、<code>/var/lib/mysql</code>には実体は存在せず、あくまでも volume にデータが保存される。そして volume はコンテナではなくホストマシンに存在するため、このコンテナが破棄されたところで、データは残り続ける。<br />
MySQL コンテナの場合、データは<code>/var/lib/mysql</code>に保存される。そのため SQL でデータに対して何らかの操作を行うと、<code>/var/lib/mysql</code>に対して操作を行うことになり、それはつまり volume に対して操作を行うことになる。</p>
<p>実際に操作を行って確認してみる。</p>
<p>まず<code>first_db</code>に接続して<code>sample_db</code>というデータベースを作る。</p>
<pre class="code" data-lang="" data-unlink>% docker exec -it first_db mysql -p
mysql> CREATE DATABASE sample_db;
Query OK, 1 row affected (0.01 sec)
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sample_db |
| sys |
+--------------------+
5 rows in set (0.00 sec)</pre>
<p>次に<code>first_db</code>を削除する。</p>
<pre class="code" data-lang="" data-unlink>% docker stop first_db
% docker rm first_db</pre>
<p>そして<code>second_db</code>という新しいコンテナを作る。この際、先程と同様に<code>mysql_volume</code>をコンテナと紐付けるようにする。</p>
<pre class="code" data-lang="" data-unlink>% docker run --name second_db -dit -e MYSQL_ROOT_PASSWORD=password -v mysql_volume:/var/lib/mysql mysql:8</pre>
<p>そうすると、既に<code>second_db</code>のなかに<code>sample_db</code>が存在している。</p>
<pre class="code" data-lang="" data-unlink>% docker exec -it second_db mysql -p
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sample_db |
| sys |
+--------------------+
5 rows in set (0.01 sec)</pre>
<p><code>first_db</code>に対して行った操作(<code>sample_db</code>の作成)は<code>mysql_volume</code>という volume に保存され、そのデータは<code>first_db</code>とは別に管理されている。そのため、<code>first_db</code>が削除されても残り続ける。<br />
そして<code>second_db</code>を作成する際に<code>mysql_volume</code>と紐付けたので、既に<code>second_db</code>には<code>sample_db</code>が存在しているのである。</p>
<h2>network によるコンテナ間の通信</h2>
<p>network という機能を使うと、コンテナ間で通信ができるようになる。<br />
例として Nginx のコンテナと Node.js のコンテナを用意し、両者間で通信できるようにしてみる。</p>
<h3>Nginx コンテナの準備</h3>
<p>まず<code>nginx</code>ディレクトリと<code>node</code>ディレクトリを作り、そこに必要なファイルを入れていくことにする。</p>
<pre class="code" data-lang="" data-unlink>% mkdir nginx
% mkdir node</pre>
<p>そして以下のコマンドを実行し、<code>nginx/user</code>ディレクトリにデータを用意する。このデータを Nginx コンテナにコピーして使うことになる。</p>
<pre class="code" data-lang="" data-unlink>% mkdir nginx/user
% echo "{\"id\": 1, \"name\": \"Alice\"}" > nginx/user/1.json
% echo "{\"id\": 2, \"name\": \"Bob\"}" > nginx/user/2.json
% echo "{\"id\": 3, \"name\": \"Carol\"}" > nginx/user/3.json</pre>
<p>最後に、以下の内容の<code>nginx/Dockerfile</code>を作成。</p>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>nginx:latest
<span class="synStatement">WORKDIR </span>/usr/share/nginx/html
<span class="synStatement">COPY </span>./user ./user
</pre>
<p>Dockerfile の書き方については特に説明しないので、下記を参照。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2022%2F04%2F11%2F002854" title="Dockerfile に入門して Node.js アプリを作ってみる - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://numb86-tech.hatenablog.com/entry/2022/04/11/002854">numb86-tech.hatenablog.com</a></cite></p>
<p>そして以下のコマンドを実行し、<code>sample_nginx_image</code>というイメージを作る。</p>
<pre class="code" data-lang="" data-unlink>% docker build -t sample_nginx_image ./nginx</pre>
<p>そしてそのイメージから、<code>sample_nginx</code>というコンテナを作る。</p>
<pre class="code" data-lang="" data-unlink>% docker run --name sample_nginx -d -p 8084:80 sample_nginx_image</pre>
<p><code>8084:80</code>でポートマッピングしたので、ホストマシンの<code>8084</code>ポートを経由して、Nginx コンテナの<code>80</code>ポートにアクセスできる。</p>
<pre class="code" data-lang="" data-unlink>% curl localhost:8084/user/1.json
{"id": 1, "name": "Alice"}
% curl localhost:8084/user/2.json
{"id": 2, "name": "Bob"}
% curl localhost:8084/user/3.json
{"id": 3, "name": "Carol"}
% curl localhost:8084/user/4.json
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.21.3</center>
</body>
</html></pre>
<p>だがこれは、ホストマシンとコンテナの通信である。<br />
繰り返しになるが、network を使うことで、コンテナとコンテナで通信できるようになる。</p>
<p>Nginx コンテナが問題なく動いていることを確認できたので、<code>sample_nginx</code>は、停止、削除しておく。</p>
<pre class="code" data-lang="" data-unlink>% docker stop sample_nginx
% docker rm sample_nginx</pre>
<h3>network の作成</h3>
<p><code>docker network create ネットワークの名前</code>で、ネットワークを作成できる。</p>
<pre class="code" data-lang="" data-unlink>% docker network create sample_network</pre>
<p>そして作成されたネットワークとコンテナを紐付けることで、同一のネットワークに紐付けられているコンテナ同士で通信できるようになる。</p>
<p>まず、<code>sample_nginx_image</code>を元にしたコンテナを、<code>sample_network</code>と紐付ける形で作成する。<br />
<code>--net ネットワークの名前</code>オプションを付けることで、そのネットワークと紐付ける形でコンテナを作れる。</p>
<pre class="code" data-lang="" data-unlink>% docker run --name sample_nginx -d --net sample_network sample_nginx_image</pre>
<h3>Node.js コンテナの作成</h3>
<p>あとは、<code>sample_network</code>と紐付ける形で、Node.js のコンテナを作ればよい。</p>
<p>以下の内容の<code>node/index.js</code>と<code>node/package.json</code>を作る。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> fetch from <span class="synConstant">"node-fetch"</span>;
<span class="synStatement">import</span> http from <span class="synConstant">"http"</span>;
http
.createServer(async <span class="synIdentifier">function</span> (req, res) <span class="synIdentifier">{</span>
<span class="synIdentifier">let</span> result;
<span class="synStatement">try</span> <span class="synIdentifier">{</span>
<span class="synStatement">const</span> fetchResponse = await fetch(<span class="synConstant">"http://sample_nginx:80/user/1.json"</span>); <span class="synComment">// 通信するコンテナの名前を host 部分に書く</span>
result = await fetchResponse.json();
<span class="synIdentifier">}</span> <span class="synStatement">catch</span> (e) <span class="synIdentifier">{</span>
result = e.message;
<span class="synIdentifier">}</span>
res.writeHead(200, <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"application/json"</span> <span class="synIdentifier">}</span>);
res.end(`$<span class="synIdentifier">{</span>JSON.stringify(result)<span class="synIdentifier">}\</span>n`);
<span class="synIdentifier">}</span>)
.listen(3000);
</pre>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">type</span>": "<span class="synConstant">module</span>",
"<span class="synStatement">scripts</span>": <span class="synSpecial">{</span>
"<span class="synStatement">start</span>": "<span class="synConstant">node index.js</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">dependencies</span>": <span class="synSpecial">{</span>
"<span class="synStatement">node-fetch</span>": "<span class="synConstant">3.2.3</span>"
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p>重要なのは<code>node/index.js</code>の<code>fetch</code>の部分。<br />
ここで、リクエスト先のホストを<code>sample_nginx</code>としている。こうすることで、<code>sample_nginx</code>コンテナと通信が行われる。</p>
<p>最後に<code>node/Dockerfile</code>を作成し、そこから<code>sample_node_image</code>というイメージを作成する。</p>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:16
<span class="synStatement">WORKDIR </span>/scripts
<span class="synStatement">COPY </span>package.json ./
<span class="synStatement">RUN </span>npm i -G yarn
<span class="synStatement">RUN </span>yarn install
<span class="synStatement">COPY </span>index.js ./
<span class="synStatement">CMD </span>[<span class="synConstant">"yarn"</span>, <span class="synConstant">"run"</span>, <span class="synConstant">"start"</span>]
</pre>
<pre class="code" data-lang="" data-unlink>% docker build -t sample_node_image ./node</pre>
<p>そして、<code>sample_network</code>と紐付ける形でコンテナを作成、起動する。</p>
<pre class="code" data-lang="" data-unlink>% docker run --name sample_node --net sample_network -d -p 8080:3000 sample_node_image</pre>
<p><code>sample_nginx</code>も<code>sample_node</code>も<code>sample_network</code>に紐付けられているため、両者間で通信ができるようになり、<code>fetch</code>で情報を取得できるようになった。</p>
<p>curl で Node.js コンテナにアクセスしてみると、<code>sample_nginx</code>から取得したデータが返されていることを確認できる。</p>
<pre class="code" data-lang="" data-unlink>% curl http://localhost:8080
{"id":1,"name":"Alice"}</pre>
<h2>Node.js コンテナと MySQL コンテナを接続する</h2>
<p>最後により実践的な内容として、Node.js コンテナと MySQL コンテナの間でやり取りできるようにしてみる。また、MySQL コンテナには volume を紐付け、データが永続化されるようにする。</p>
<p>まずは volume と network の作成。</p>
<pre class="code" data-lang="" data-unlink>% docker volume create app_volume
% docker network create app_network</pre>
<p>そして、これらと紐付けた形で、MySQL コンテナを作成する。</p>
<pre class="code" data-lang="" data-unlink>% docker run --name app_db -dit -e MYSQL_ROOT_PASSWORD=password -v app_volume:/var/lib/mysql --net app_network mysql:8</pre>
<p>そして作成されたコンテナの MySQL に接続し、動作確認用のテーブルやデータを用意する。</p>
<pre class="code" data-lang="" data-unlink>% docker exec -it app_db mysql -p
mysql> CREATE DATABASE sample_db;
Query OK, 1 row affected (0.00 sec)
mysql> USE sample_db;
Database changed
mysql> CREATE TABLE users (id INT AUTO_INCREMENT, name TEXT NOT NULL, PRIMARY KEY (id));
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO users(name) VALUES('Alice');
Query OK, 1 row affected (0.02 sec)</pre>
<p>具体的には、<code>sample_db</code>というデータベースを作成し、そのなかに<code>users</code>というテーブルを作成、そしてそこに 1 件のレコードを作成している。</p>
<pre class="code" data-lang="" data-unlink>mysql> SELECT * FROM users;
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
+----+-------+
1 row in set (0.00 sec)</pre>
<p>Node.js コンテナからこのデータを取得できるようにするのが、ゴールである。</p>
<p>あとで必要になるのでポート番号も確認しておく。</p>
<pre class="code" data-lang="" data-unlink>mysql> show variables like 'port';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| port | 3306 |
+---------------+-------+
1 row in set (0.01 sec)</pre>
<p>MySQL 側の準備はこれで完了。</p>
<p>次は、Node.js コンテナの準備。<br />
以下の内容で<code>index.js</code>、<code>package.json</code>、<code>Dockerfile</code>を作成する。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> fetch from <span class="synConstant">"node-fetch"</span>;
<span class="synStatement">import</span> http from <span class="synConstant">"http"</span>;
<span class="synStatement">import</span> mysql from <span class="synConstant">"mysql2"</span>;
http
.createServer(async <span class="synIdentifier">function</span> (req, res) <span class="synIdentifier">{</span>
<span class="synIdentifier">let</span> result;
<span class="synStatement">try</span> <span class="synIdentifier">{</span>
<span class="synStatement">const</span> connection = mysql.createConnection(<span class="synIdentifier">{</span>
host: <span class="synConstant">"app_db"</span>, <span class="synComment">// MySQL のコンテナの名前</span>
port: 3306, <span class="synComment">// 先程調べたポート番号</span>
user: <span class="synConstant">"root"</span>,
password: <span class="synConstant">"password"</span>,
database: <span class="synConstant">"sample_db"</span>, <span class="synComment">// 接続したいデータベースの名前</span>
<span class="synIdentifier">}</span>);
connection.connect();
result = await connection
.promise()
.query(<span class="synConstant">"SELECT * FROM users;"</span>)
.then((<span class="synIdentifier">[</span>rows<span class="synIdentifier">]</span>) => <span class="synIdentifier">{</span>
<span class="synStatement">return</span> rows;
<span class="synIdentifier">}</span>)
.<span class="synStatement">catch</span>((err) => err);
<span class="synIdentifier">}</span> <span class="synStatement">catch</span> (e) <span class="synIdentifier">{</span>
result = e.message;
<span class="synIdentifier">}</span>
res.writeHead(200, <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"application/json"</span> <span class="synIdentifier">}</span>);
res.end(`$<span class="synIdentifier">{</span>JSON.stringify(result)<span class="synIdentifier">}\</span>n`);
<span class="synIdentifier">}</span>)
.listen(3000);
</pre>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">type</span>": "<span class="synConstant">module</span>",
"<span class="synStatement">license</span>": "<span class="synConstant">MIT</span>",
"<span class="synStatement">scripts</span>": <span class="synSpecial">{</span>
"<span class="synStatement">start</span>": "<span class="synConstant">node index.js</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">dependencies</span>": <span class="synSpecial">{</span>
"<span class="synStatement">mysql2</span>": "<span class="synConstant">2.3.3</span>",
"<span class="synStatement">node-fetch</span>": "<span class="synConstant">3.2.3</span>"
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:16
<span class="synStatement">WORKDIR </span>/scripts
<span class="synStatement">COPY </span>package.json ./
<span class="synStatement">RUN </span>npm i -G yarn
<span class="synStatement">RUN </span>yarn install
<span class="synStatement">COPY </span>index.js ./
<span class="synStatement">CMD </span>[<span class="synConstant">"yarn"</span>, <span class="synConstant">"run"</span>, <span class="synConstant">"start"</span>]
</pre>
<p><code>index.js</code>のなかで MySQL との接続を行っているが、その際に<code>host</code>として<code>app_db</code>を指定することで、<code>app_db</code>コンテナと通信できるようになる。もちろん、Node.js コンテナと<code>app_db</code>コンテナが同一のネットワーク(今回の場合は<code>app_network</code>)に紐付いていることが前提となる。</p>
<p>イメージを作成し、そこからコンテナを作成、起動する。</p>
<pre class="code" data-lang="" data-unlink>$ docker build -t app_node_image .
$ docker run --name app_node -d -p 8080:3000 --net app_network app_node_image</pre>
<p>無事、Node.js を経由して MySQL からデータを取得することができた。</p>
<pre class="code" data-lang="" data-unlink>% curl http://localhost:8080
[{"id":1,"name":"Alice"}]`</pre>
numb_86
Dockerfile に入門して Node.js アプリを作ってみる
hatenablog://entry/13574176438081764209
2022-04-11T00:28:54+09:00
2022-04-11T00:28:54+09:00 Docker への入門の一環として、自分で Dockerfile を作成し、それを使って Node.js アプリを Docker Container で動かしてみる。 Hello World Dockerfile を使うことで、既存の Docker Image を編集して新しい Docker Image を作ることができる。 具体的には、Dockerfileという名前のファイルにコマンドを記述していくことで、その内容に基づいた Docker Image を作成できるようになる。 例えば以下の Dockerfile では、FROMとCMDというコマンドを使っている。これらのコマンドの意味は後述す…
<p>Docker への入門の一環として、自分で Dockerfile を作成し、それを使って Node.js アプリを Docker Container で動かしてみる。</p>
<h2>Hello World</h2>
<p>Dockerfile を使うことで、既存の Docker Image を編集して新しい Docker Image を作ることができる。<br />
具体的には、<code>Dockerfile</code>という名前のファイルにコマンドを記述していくことで、その内容に基づいた Docker Image を作成できるようになる。</p>
<p>例えば以下の Dockerfile では、<code>FROM</code>と<code>CMD</code>というコマンドを使っている。これらのコマンドの意味は後述する。</p>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:16
<span class="synStatement">CMD </span>[ <span class="synConstant">"echo"</span>, <span class="synConstant">"Hello World"</span> ]
</pre>
<p>カレントディレクトリに上記の<code>Dockerfile</code>がある状態で<code>% docker build -t sample .</code>を実行すると、<code>sample</code>という名前の Docker Image が作成される。名前はもちろん<code>sample</code>以外でも構わない。<br />
作られた<code>sample</code>から Docker Container を作成し実行すると、<code>Hello World</code>が表示される。ちなみに<code>--rm</code>オプションを付けておくと、Docker Container の停止時にその Docker Container を自動的に削除してくれる。</p>
<pre class="code" data-lang="" data-unlink>% docker run --rm sample
Hello World</pre>
<p>このように、</p>
<ol>
<li><code>Dockerfile</code>というファイルにコマンドを記述する</li>
<li><code>Dockerfile</code>に基づいて Docker Image を作成する</li>
<li>作られた Docker Image から Docker Container を作成、実行する</li>
</ol>
<p>というのが基本的な流れになる。</p>
<p>Dockerfile コマンドを知らないと意図した Docker Image は作れないので、まずはコマンドについて簡単に見ていく。</p>
<h2>FROM</h2>
<p>ゼロから Docker Image を作ることはまずなく、既存の Docker Image をベースして、そこにコマンドによるカスタマイズを重ねてオリジナルの Docker Image を作ることになる。<br />
そのベースとなる Docker Image を指定するのが、<code>FROM</code>コマンド。</p>
<p>先程は<code>FROM node:16</code>と記述したが、このようにすると公式が配布している Node.js <code>v16</code>の Docker Image がベースになる。</p>
<h2>CMD</h2>
<p>これは、Docker Container の起動時に実行するコマンドを指定するためのコマンド。</p>
<p>そのため、以下のようにすると、Docker Container の起動時に Node.js のバージョンが表示される。</p>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:16
<span class="synStatement">CMD </span>[ <span class="synConstant">"node"</span>, <span class="synConstant">"--version"</span> ]
</pre>
<p>実際に試してみる。<br />
まずは Docker Image の作成。</p>
<pre class="code" data-lang="" data-unlink>% docker build -t sample .</pre>
<p><code>sample</code>という Docker Image は先程も作成したので既に存在するが、新しい内容で<code>sample</code>という Docker Image が改めて作られるので、問題ない。<br />
<code>sample</code>から Docker Container を作成、実行すると、Node.js のバージョンが表示される。</p>
<pre class="code" data-lang="" data-unlink>% docker run --rm sample
v16.14.2</pre>
<h3>Docker Image の後片付け</h3>
<p>上述したように、既に存在する Docker Image と同名の Docker Image を作成しようとした場合、特にエラーになることはなく、問題なく作成される。<br />
ではこれまで存在した Docker Image はどうなるかというと、<code><none></code>という名前に変化する。<br />
<code>% docker images</code>で調べてみると、<code><none></code>という名前の Docker Image が作られているはず。</p>
<p>この記事では基本的に<code>sample</code>という名前で Docker Image を作っていく。<br />
そうすると、<code><none></code>という名前の Docker Image がどんどん増えていく。<br />
使うことのない Docker Image が増えても容量を圧迫するだけなので、適宜削除していく必要がある。</p>
<p>ひとつひとつ削除してもいいのだが、以下のコマンドを実行することで<code><none></code>という名前の Docker Image を一括で削除できる。</p>
<pre class="code" data-lang="" data-unlink>% docker rmi $(docker images -f "dangling=true" -q)</pre>
<h2>COPY</h2>
<p>先程紹介した<code>CMD</code>コマンドは、Dockerfile にひとつしか書くことができない。<br />
では、Docker Container の起動時に複数のコマンドを実行させたい場合は、どうすればいいのか。<br />
シェルスクリプトを使えばよい。シェルスクリプトに実行させたいコマンドを書いておき、<code>CMD</code>コマンドでそのシェルスクリプトを実行することで、複数のコマンドを実行できる。</p>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">CMD </span>[<span class="synConstant">"bash"</span>, <span class="synConstant">"shell.sh"</span>]
</pre>
<p>だがそのためには、任意のシェルスクリプト(上記の例だと<code>shell.sh</code>)を、Docker Image に含めないといけない。<br />
そういった時に使うのが、<code>COPY</code>コマンドである。<br />
<code>COPY</code>コマンドを使うと、ホストにあるファイルを Docker Image にコピーすることができる。</p>
<p>まず、以下の内容の<code>shell.sh</code>というファイルをカレントディレクトリに用意する。</p>
<pre class="code shell" data-lang="shell" data-unlink>#!/bin/bash
echo 1
echo 2
echo 3</pre>
<p>次に、Dockerfile の内容を以下のようにして、<code>% docker build -t sample .</code>でビルドする。</p>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:16
<span class="synStatement">COPY </span>shell.sh ./
<span class="synStatement">CMD </span>[ <span class="synConstant">"ls"</span> ]
</pre>
<p>ビルドされた Docker Image から Docker Container を起動すると<code>ls</code>コマンドが実行されるが、既存のディレクトリに加えて<code>shell.sh</code>も存在していることが分かる。</p>
<pre class="code" data-lang="" data-unlink>% docker run --rm sample
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
shell.sh
srv
sys
tmp
usr
var</pre>
<p>Dockerfile を以下のように書き換えてビルド、実行すると、<code>shell.sh</code>の中身が実行される。</p>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:16
<span class="synStatement">COPY </span>shell.sh ./
<span class="synStatement">CMD </span>[<span class="synConstant">"bash"</span>, <span class="synConstant">"shell.sh"</span>]
</pre>
<pre class="code" data-lang="" data-unlink>% docker run --rm sample
1
2
3</pre>
<p><code>COPY</code>コマンドは、ひとつめの引数でホスト側のパスを指定し、ふたつめの引数で Docker Image のパスを指定する。</p>
<p>ホスト側のパスの基準となるのは、<code>% docker build -t sample .</code>の<code>.</code>の部分。ここで基準となるパスを指定する。</p>
<p>例えば、カレントディレクトリにある<code>material</code>というディレクトリに<code>a.txt</code>、<code>b.txt</code>、<code>c.txt</code>がある場合、以下のどちらの組み合わせでも、3 つのファイルを Docker Image にコピーできる。</p>
<ul>
<li><code>COPY ./material ./</code>と記述し、<code>% docker build -t sample .</code>でビルドする
<ul>
<li>基準となるパスが<code>.</code>であり、<code>COPY</code>コマンドの引数が<code>./material</code>なので、<code>./material</code>内にあるファイルがコピーされる</li>
</ul>
</li>
<li><code>COPY ./ ./</code>と記述し、<code>% docker build -t sample -f Dockerfile ./material</code>でビルドする
<ul>
<li>基準となるパスが<code>./material</code>であり、<code>COPY</code>コマンドの引数が<code>./</code>なので、<code>./material</code>内にあるファイルがコピーされる</li>
<li>デフォルトだとビルド時に指定されたパス(このケースだと<code>./material</code>)にある Dockerfile を探してしまうため、<code>-f</code>オプションで明示的に Dockerfile を指定している</li>
</ul>
</li>
</ul>
<p>では、Docker Image のパスの基準は、どのように決まるのか。それを指定するのが、<code>WORKDIR</code>コマンドである。</p>
<h2>WORKDIR</h2>
<p><code>WORKDIR</code>コマンドで指定したディレクトリが、Dockerfile でコマンドを実行する際の基準になる。<br />
<code>COPY</code>にしろ<code>CMD</code>にしろ、<code>WORKDIR</code>で指定したディレクトリが基準になる。<br />
指定したディレクトリが既存の Docker Image に存在しなかった場合、自動的に作成される。</p>
<p>例えば以下の内容でビルド、実行を行うと、<code>/foo/bar</code>と表示される。<code>/foo/bar</code>で<code>pwd</code>コマンドを実行したためである。</p>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:16
<span class="synStatement">WORKDIR </span>/foo/bar
<span class="synStatement">CMD </span>[ <span class="synConstant">"pwd"</span> ]
</pre>
<p><code>WORKDIR</code>コマンドを使わなかった場合は、これまで見てきたように<code>/</code>が基準のディレクトリとなる。</p>
<h2>RUN</h2>
<p>Docker Image のビルド時に実行するコマンドを指定するためのコマンド。<br />
<code>CMD</code>は Docker Container の起動時に実行するコマンドを指定するためのものなので、そこが異なる。<br />
また、<code>CMD</code>と違って複数書くことができる。<br />
<code>RUN</code>で実行するコマンドの基準となるディレクトリも、<code>WORKDIR</code>で指定したディレクトリになる。</p>
<h2>Node.js でウェブアプリを作る</h2>
<p>最後に Dockerfile の実践として、Node.js アプリが起動する Docker Container を作ることにする。<br />
アプリはどんなものでも構わないのだが、TypeScript で書かれた、<code>Hello World</code>を返すだけのウェブアプリにする。</p>
<p>まず、Docker は意識せずにアプリを書いていく。</p>
<p>必要なライブラリをインストール。</p>
<pre class="code" data-lang="" data-unlink>% yarn init -y
% yarn add -D typescript ts-node-dev @types/node@16</pre>
<p><code>tsconfig.json</code>を用意。</p>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">compilerOptions</span>": <span class="synSpecial">{</span>
"<span class="synStatement">sourceMap</span>": <span class="synConstant">true</span>,
"<span class="synStatement">outDir</span>": "<span class="synConstant">dist</span>",
"<span class="synStatement">strict</span>": <span class="synConstant">true</span>,
"<span class="synStatement">lib</span>": <span class="synSpecial">[</span>"<span class="synConstant">esnext</span>", "<span class="synConstant">DOM</span>"<span class="synSpecial">]</span>,
"<span class="synStatement">esModuleInterop</span>": <span class="synConstant">true</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p><code>index.ts</code>を用意。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> http <span class="synStatement">from</span> <span class="synConstant">"http"</span><span class="synStatement">;</span>
http
.createServer<span class="synStatement">(function</span> <span class="synStatement">(</span>_<span class="synStatement">,</span> res<span class="synStatement">)</span> <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synConstant">"Content-Type"</span>: <span class="synConstant">"text/plain"</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
res.end<span class="synStatement">(</span><span class="synConstant">"Hello World\n"</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
.listen<span class="synStatement">(</span><span class="synConstant">3000</span><span class="synStatement">);</span>
</pre>
<p><code>package.json</code>の<code>scripts</code>に<code>"start": "ts-node-dev index.ts"</code>を書き加える。</p>
<p>この状態で<code>% yarn run start</code>を実行するとウェブサーバが起動するはずなので、curl で動作確認してみる。</p>
<pre class="code" data-lang="" data-unlink>% curl http://localhost:3000
Hello World</pre>
<p>問題なく動いているので、ここから、Docker で動かしていく。</p>
<h3>Dockerfile</h3>
<p>まずは Dockerfile を書く。</p>
<pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:16
<span class="synStatement">WORKDIR </span>/scripts
<span class="synStatement">COPY </span>package.json ./
<span class="synStatement">COPY </span>yarn.lock ./
<span class="synStatement">COPY </span>tsconfig.json ./
<span class="synStatement">RUN </span>npm i -G yarn
<span class="synStatement">RUN </span>yarn install --frozen-lockfile
<span class="synStatement">COPY </span>index.ts ./
<span class="synStatement">CMD </span>[<span class="synConstant">"yarn"</span>, <span class="synConstant">"run"</span>, <span class="synConstant">"start"</span>]
</pre>
<p>必要なファイルをコピーしたあと、ライブラリのインストールを行う。<br />
その後<code>index.ts</code>をコピーし、Docker Container の起動時に<code>yarn run start</code>が実行されるようにする。</p>
<h3>Docker Image のビルドと Docker Container の起動</h3>
<p>ビルドは今まで通り<code>% docker build -t sample .</code>でよい。</p>
<p>Docker Container の起動は、以下のようにする。</p>
<pre class="code" data-lang="" data-unlink>% docker run --rm -d -p 8080:3000 sample</pre>
<p><code>-d</code>で、Docker Container がバックグラウンドで実行されるようにする。<br />
そして<code>-p 8080:3000</code>で、ホスト側の<code>8080</code>ポートと Docker Container 側の<code>3000</code>ポートをマッピングする。<br />
これで、ホストの<code>8080</code>ポートを通して、Docker Container の<code>3000</code>ポートにアクセスできる。</p>
<pre class="code" data-lang="" data-unlink>% curl http://localhost:8080
Hello World</pre>
<p>最後に、このままだとずっと Docker Container が起動したままなので、<code>% docker ps</code>で Docker Container で名前を調べ、<code>% docker stop コンテナ名</code>で停止させておく。</p>
<h2>参考資料</h2>
<ul>
<li><a href="https://y-ohgi.com/introduction-docker/2_component/dockerfile/">dockerfile - 入門 Docker</a></li>
</ul>
numb_86
Prisma に入門して API サーバを作ってみる
hatenablog://entry/13574176438076862525
2022-03-26T18:00:52+09:00
2022-03-26T18:00:52+09:00 Prisma は、Node.js の ORM。 この記事では、導入方法、基本的な使い方について説明したのち、Prisma を使って簡単な API サーバを作ってみる。 Node.js のバージョンは16.13.2、MySQLのバージョンは8.0.28という環境で、動作確認している。 使用している npm ライブラリのバージョンは以下の通り。 @prisma/client@3.11.0 @types/node@16.11.26 prisma@3.11.0 ts-node-dev@1.1.8 typescript@4.6.2 MySQL のインストール ORM である Prisma を試すためには…
<p>Prisma は、Node.js の ORM。<br />
この記事では、導入方法、基本的な使い方について説明したのち、Prisma を使って簡単な API サーバを作ってみる。</p>
<p>Node.js のバージョンは<code>16.13.2</code>、MySQLのバージョンは<code>8.0.28</code>という環境で、動作確認している。</p>
<p>使用している npm ライブラリのバージョンは以下の通り。</p>
<ul>
<li>@prisma/client@3.11.0</li>
<li>@types/node@16.11.26</li>
<li>prisma@3.11.0</li>
<li>ts-node-dev@1.1.8</li>
<li>typescript@4.6.2</li>
</ul>
<h2>MySQL のインストール</h2>
<p>ORM である Prisma を試すためには、まずデータベースをセットアップしないといけない。今回は MySQL を使うことにする。<br />
この記事では Homebrew を使ってインストールしているが、他の方法でももちろん問題ない。</p>
<pre class="code" data-lang="" data-unlink>% brew install mysql</pre>
<p>バージョンを確認できればインストール成功。</p>
<pre class="code" data-lang="" data-unlink>% mysql --version
mysql Ver 8.0.28 for macos11.6 on x86_64 (Homebrew)</pre>
<p><code>brew services start mysql</code>で起動、<code>brew services stop mysql</code>で終了。<br />
以降、MySQL が起動しているという前提で話を進めていく。</p>
<p>以下のコマンドを打つといくつか質問されるので、root ユーザーのパスワード設定以外は全て No にする。Enter を押していけば No になるはず。</p>
<pre class="code" data-lang="" data-unlink>% mysql_secure_installation</pre>
<p>以下のコマンドを打つと root ユーザーのパスワードを求められるので、先程設定したパスワードを入力すると、ログインできる。</p>
<pre class="code" data-lang="" data-unlink>% mysql --user=root --password</pre>
<h2>データベースの準備</h2>
<p>MySQL にログインしたら、Prisma で操作するデータベースを用意する。<br />
今回は<code>prisma_db</code>という名前にしたが、特に制限はない。</p>
<pre class="code" data-lang="" data-unlink>mysql> CREATE DATABASE prisma_db;</pre>
<p><code>SHOW DATABASES;</code>で確認して<code>prisma_db</code>が存在すれば成功。</p>
<pre class="code" data-lang="" data-unlink>mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| prisma_db |
| sys |
+--------------------+
5 rows in set (0.00 sec)</pre>
<p><code>USE prisma_db;</code>とすると、以降は<code>prisma_db</code>に対して操作できるようになる。</p>
<p><code>SHOW TABLES;</code>を実行すると、<code>prisma_db</code>にはまだテーブルがないことを確認できる。</p>
<pre class="code" data-lang="" data-unlink>mysql> SHOW TABLES;
Empty set (0.00 sec)</pre>
<p>最後に、このデータベースのポート番号を確認する。Prisma の設定時に必要になる。</p>
<pre class="code" data-lang="" data-unlink>mysql> show variables like 'port';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| port | 3306 |
+---------------+-------+
1 row in set (0.00 sec)</pre>
<h2>Prisma プロジェクトを作成する</h2>
<p>まずは必要なライブラリをインストールする。</p>
<pre class="code" data-lang="" data-unlink>% yarn init -y
% yarn add -D typescript ts-node-dev prisma</pre>
<p>TypeScript で開発していくので、以下の<code>tsconfig.json</code>を作る。</p>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">compilerOptions</span>": <span class="synSpecial">{</span>
"<span class="synStatement">sourceMap</span>": <span class="synConstant">true</span>,
"<span class="synStatement">outDir</span>": "<span class="synConstant">dist</span>",
"<span class="synStatement">strict</span>": <span class="synConstant">true</span>,
"<span class="synStatement">lib</span>": <span class="synSpecial">[</span>"<span class="synConstant">esnext</span>", "<span class="synConstant">DOM</span>"<span class="synSpecial">]</span>,
"<span class="synStatement">esModuleInterop</span>": <span class="synConstant">true</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p>以下のコマンドを実行すると、いくつかのファイルが生成される。</p>
<pre class="code" data-lang="" data-unlink>% yarn run prisma init</pre>
<p>まず<code>.env</code>。<br />
このファイルで<code>DATABASE_URL</code>という環境変数が定義されているので、これを自身の環境に合わせて書き換える。<br />
この記事の内容に沿って環境を構築した場合、以下のようになるはず。<code>YOURPASSWORD</code>の部分だけ、先程設定した root ユーザーのパスワードに書き換えればよい。</p>
<pre class="code" data-lang="" data-unlink>DATABASE_URL="mysql://root:YOURPASSWORD@localhost:3306/prisma_db"</pre>
<p>そして、<code>prisma/schema.prisma</code>という名前のスキーマファイルも生成される。<br />
以下の内容になっているはずなので、<code>postgresql</code>の部分を<code>mysql</code>に書き換える。</p>
<pre class="code prisma" data-lang="prisma" data-unlink>// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}</pre>
<h2>モデルを定義する</h2>
<p>環境構築が終わったのでここから実際に開発を進めていくが、まずはモデルを定義する。</p>
<p>先程編集したスキーマファイルに、モデルの定義を書き足していく。<br />
スキーマファイルは<code>Prisma Schema Language(PSL)</code>という構文で書いていく。<br />
また、<code>.prisma</code>ファイルは、<code>% yarn run prisma format</code>を実行することでフォーマットできる。</p>
<p>早速、<code>User</code>というモデルを定義してみる。</p>
<pre class="code prisma" data-lang="prisma" data-unlink>model User {
id Int @id @default(autoincrement())
name String
}</pre>
<p>そして、以下のコマンドを実行する。</p>
<pre class="code" data-lang="" data-unlink>% yarn run prisma migrate dev --name init</pre>
<p>そうすると様々な処理が行われるが、まず、<code>@prisma/client</code>がまだインストールされていなかった場合はインストールされる。<code>package.json</code>に追記されていることを確認できる。<br />
それから、<code>prisma/migrations</code>ディレクトリに、マイグレーションファイルが作成される。<br />
そしてそのマイグレーションファイルをデータベースに対して実行する。</p>
<p><code>prisma_db</code>の中身を見てみると、<code>User</code>テーブルが作られているのが分かる。</p>
<pre class="code" data-lang="" data-unlink>mysql> SHOW TABLES;
+---------------------+
| Tables_in_prisma_db |
+---------------------+
| _prisma_migrations |
| User |
+---------------------+</pre>
<p>このように、Prisma のモデルはデータベースのテーブルに対応している。</p>
<p><code>DESC</code>で調べてみると、モデルの定義通りに作成されている。</p>
<pre class="code" data-lang="" data-unlink>mysql> DESC User;
+-------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(191) | NO | | NULL | |
+-------+--------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)</pre>
<p>この時点ではレコードはまだ存在しない。</p>
<pre class="code" data-lang="" data-unlink>mysql> SELECT * FROM User;
Empty set (0.00 sec)</pre>
<h2>Prisma クライアントを使った CRUD 操作</h2>
<p>Prisma クライアントを使ってコードを書いていくことで、データベースに対する操作を行うことができる。<br />
ここでは、User テーブルに対して簡単な CRUD 操作を行っていく。</p>
<p>最初に、動作確認用の雛形となるコードを用意する。<br />
<code>src/index.ts</code>という名前で以下のファイルを作成する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> PrismaClient <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"@prisma/client"</span><span class="synStatement">;</span>
<span class="synType">const</span> prisma <span class="synStatement">=</span> <span class="synStatement">new</span> PrismaClient<span class="synStatement">();</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synComment">// ここにクエリを書いていく</span>
<span class="synIdentifier">}</span>
main<span class="synStatement">()</span>
.<span class="synSpecial">catch</span><span class="synStatement">((</span>e<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">throw</span> e<span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
.<span class="synSpecial">finally</span><span class="synStatement">(async</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synComment">// データベースとのコネクションを切る</span>
<span class="synStatement">await</span> prisma.$disconnect<span class="synStatement">();</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<p>あとは<code>main</code>関数のなかに具体的な処理を書き、<code>% yarn run ts-node-dev src/index.ts</code>で実行すればいい。</p>
<h3>レコードの作成</h3>
<p>レコードを作成するには<code>create</code>メソッドを使う。<code>create</code>の返り値は、作成された<code>User</code>のオブジェクト。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.create<span class="synStatement">(</span><span class="synIdentifier">{</span>
data: <span class="synIdentifier">{</span>
name: <span class="synConstant">"Alice"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span> <span class="synComment">// { id: 1, name: 'Alice' }</span>
<span class="synIdentifier">}</span>
</pre>
<p><code>% yarn run ts-node-dev src/index.ts</code>を実行したあとにデータベースで検索を行うと、レコードが作られていることがわかる。</p>
<pre class="code" data-lang="" data-unlink>mysql> SELECT * FROM User;
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
+----+-------+
1 row in set (0.00 sec)</pre>
<p><code>data</code>に渡すフィールドがモデルの定義と一致していない場合、型エラーとなる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// 以下は全て型エラー</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">await</span> prisma.user.create<span class="synStatement">(</span><span class="synIdentifier">{</span>
data: <span class="synIdentifier">{</span>
name: <span class="synConstant">1</span><span class="synStatement">,</span> <span class="synComment">// name の型が正しくない</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">await</span> prisma.user.create<span class="synStatement">(</span><span class="synIdentifier">{</span>
data: <span class="synIdentifier">{}</span><span class="synStatement">,</span> <span class="synComment">// name が渡されていない</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">await</span> prisma.user.create<span class="synStatement">(</span><span class="synIdentifier">{</span>
data: <span class="synIdentifier">{</span>
name: <span class="synConstant">'Alice'</span><span class="synStatement">,</span>
bar: <span class="synConstant">'buz'</span><span class="synStatement">,</span> <span class="synComment">// 不要なフィールド bar を渡している</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<p>実は、<code>% yarn run prisma migrate dev --name init</code>を実行した際に<code>node_modules</code>内に型定義ファイルが自動生成されており、それを使って型チェックが行われている。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// node_modules/.prisma/client/index.d.ts から抜粋</span>
<span class="synComment">/**</span>
<span class="synComment"> * Model User</span>
<span class="synComment"> * </span>
<span class="synComment"> */</span>
<span class="synStatement">export</span> <span class="synStatement">type</span> User <span class="synStatement">=</span> <span class="synIdentifier">{</span>
id: <span class="synType">number</span>
name: <span class="synType">string</span>
<span class="synIdentifier">}</span>
</pre>
<p><code>id</code>は auto_increment なので明示的に渡さなくてもよいのだが、もちろん渡してもよい。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.create<span class="synStatement">(</span><span class="synIdentifier">{</span>
data: <span class="synIdentifier">{</span>
id: <span class="synConstant">2</span><span class="synStatement">,</span>
name: <span class="synConstant">"Bob"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span> <span class="synComment">// { id: 2, name: 'Bob' }</span>
<span class="synIdentifier">}</span>
</pre>
<p>但し、既に存在している<code>id</code>を渡してしまうと、プライマリキーとしての制約が守られていないので、実行時エラーになる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synSpecial">try</span> <span class="synIdentifier">{</span>
<span class="synStatement">await</span> prisma.user.create<span class="synStatement">(</span><span class="synIdentifier">{</span>
data: <span class="synIdentifier">{</span>
id: <span class="synConstant">2</span><span class="synStatement">,</span>
name: <span class="synConstant">"Carol"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span> <span class="synSpecial">catch</span> <span class="synStatement">(</span>e<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synComment">// PrismaClientKnownRequestError</span>
<span class="synComment">// Unique constraint failed on the constraint: `PRIMARY`</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>e<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
</pre>
<p>一度に複数のレコードを作成するときは、<code>createMany</code>を使う。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> users <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.createMany<span class="synStatement">(</span><span class="synIdentifier">{</span>
data: <span class="synIdentifier">[</span>
<span class="synIdentifier">{</span>
name: <span class="synConstant">"Carol"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">{</span>
name: <span class="synConstant">"Dave"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">]</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>users<span class="synStatement">);</span> <span class="synComment">// { count: 2 }</span>
<span class="synIdentifier">}</span>
</pre>
<pre class="code" data-lang="" data-unlink>mysql> SELECT * FROM User;
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
| 2 | Bob |
| 3 | Carol |
| 4 | Dave |
+----+-------+
4 rows in set (0.00 sec)</pre>
<h3>レコードの取得</h3>
<p>レコードの取得を行うためのメソッドとしてここでは、<code>findMany</code>、<code>findUnique</code>、<code>findFirst</code>を紹介する。</p>
<p>まず<code>findMany</code>。これは、複数のレコードを取得するメソッド。<br />
条件を何も指定しなかった場合、全てのフィールドを持った全レコードが返ってくる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> allUsers <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findMany<span class="synStatement">();</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>allUsers<span class="synStatement">);</span>
<span class="synComment">// [</span>
<span class="synComment">// { id: 1, name: 'Alice' },</span>
<span class="synComment">// { id: 2, name: 'Bob' },</span>
<span class="synComment">// { id: 3, name: 'Carol' },</span>
<span class="synComment">// { id: 4, name: 'Dave' }</span>
<span class="synComment">// ]</span>
<span class="synIdentifier">}</span>
</pre>
<p>フィールドを指定するには、<code>select</code>を使う。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> users <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findMany<span class="synStatement">(</span><span class="synIdentifier">{</span>
select: <span class="synIdentifier">{</span>
name: <span class="synConstant">true</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>users<span class="synStatement">);</span>
<span class="synComment">// [</span>
<span class="synComment">// { name: 'Alice' },</span>
<span class="synComment">// { name: 'Bob' },</span>
<span class="synComment">// { name: 'Carol' },</span>
<span class="synComment">// { name: 'Dave' }</span>
<span class="synComment">// ]</span>
<span class="synIdentifier">}</span>
</pre>
<p>レコードの絞り込みには<code>where</code>を使う。<br />
以下は<code>contains</code>を使って、「<code>name</code>フィールドに<code>o</code>が含まれているレコード」を取得している。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> users <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findMany<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
name: <span class="synIdentifier">{</span>
contains: <span class="synConstant">"o"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>users<span class="synStatement">);</span> <span class="synComment">// [ { id: 2, name: 'Bob' }, { id: 3, name: 'Carol' } ]</span>
<span class="synIdentifier">}</span>
</pre>
<p>該当するレコードが存在しないときは空の配列を返す。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> users <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findMany<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
name: <span class="synIdentifier">{</span>
contains: <span class="synConstant">"z"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>users<span class="synStatement">);</span> <span class="synComment">// []</span>
<span class="synIdentifier">}</span>
</pre>
<p><code>contains</code>以外にも様々な検索条件がある。詳しくは公式ドキュメントを参照。<br />
<a href="https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#filter-conditions-and-operators">https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#filter-conditions-and-operators</a></p>
<p><code>OR</code>や<code>AND</code>を使うこともできる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// 「name が Al から始まる」 or 「id が 3 以上」</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> users <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findMany<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
OR: <span class="synIdentifier">[</span>
<span class="synIdentifier">{</span>
name: <span class="synIdentifier">{</span>
startsWith: <span class="synConstant">"Al"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">{</span>
id: <span class="synIdentifier">{</span>
gte: <span class="synConstant">3</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">]</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>users<span class="synStatement">);</span>
<span class="synComment">// [</span>
<span class="synComment">// { id: 1, name: 'Alice' },</span>
<span class="synComment">// { id: 3, name: 'Carol' },</span>
<span class="synComment">// { id: 4, name: 'Dave' }</span>
<span class="synComment">// ]</span>
<span class="synIdentifier">}</span>
</pre>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// 「name に o を含む」 and 「id が 3 以上」</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> users <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findMany<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
AND: <span class="synIdentifier">[</span>
<span class="synIdentifier">{</span>
name: <span class="synIdentifier">{</span>
contains: <span class="synConstant">"o"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">{</span>
id: <span class="synIdentifier">{</span>
gte: <span class="synConstant">3</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">]</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>users<span class="synStatement">);</span> <span class="synComment">// [ { id: 3, name: 'Carol' } ]</span>
<span class="synIdentifier">}</span>
</pre>
<p><code>orderBy</code>を使って並べ替えを行うこともできる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> allUsers <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findMany<span class="synStatement">(</span><span class="synIdentifier">{</span>
orderBy: <span class="synIdentifier">{</span>
id: <span class="synConstant">"desc"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>allUsers<span class="synStatement">);</span>
<span class="synComment">// [</span>
<span class="synComment">// { id: 4, name: 'Dave' },</span>
<span class="synComment">// { id: 3, name: 'Carol' },</span>
<span class="synComment">// { id: 2, name: 'Bob' },</span>
<span class="synComment">// { id: 1, name: 'Alice' }</span>
<span class="synComment">// ]</span>
<span class="synIdentifier">}</span>
</pre>
<p><code>findUnique</code>を使うと、ユニークな識別子を使って、該当するレコードを取得できる。<br />
該当するレコードが存在しない場合は<code>null</code>が返ってくる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">let</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findUnique<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
id: <span class="synConstant">1</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span> <span class="synComment">// { id: 1, name: 'Alice' }</span>
user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findUnique<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
id: <span class="synConstant">99</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span> <span class="synComment">// null</span>
<span class="synIdentifier">}</span>
</pre>
<p>ユニークではないフィールドを使おうとすると型エラーになる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findUnique<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
name: <span class="synConstant">"Alice"</span><span class="synStatement">,</span> <span class="synComment">// 型エラー</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<p><code>findFirst</code>は、最初に見つかったレコードを返す。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findFirst<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
name: <span class="synIdentifier">{</span>
contains: <span class="synConstant">"o"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span> <span class="synComment">// { id: 2, name: 'Bob' }</span>
<span class="synIdentifier">}</span>
</pre>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// orderBy で降順にしているので、{ id: 3, name: 'Carol' } が返ってくる</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.findFirst<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
name: <span class="synIdentifier">{</span>
contains: <span class="synConstant">"o"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
orderBy: <span class="synIdentifier">{</span>
id: <span class="synConstant">"desc"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span> <span class="synComment">// { id: 3, name: 'Carol' }</span>
<span class="synIdentifier">}</span>
</pre>
<h3>レコードのアップデート</h3>
<p>レコードのアップデートには、<code>update</code>メソッドを使う。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.update<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
id: <span class="synConstant">4</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
data: <span class="synIdentifier">{</span>
name: <span class="synConstant">"David"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span> <span class="synComment">// { id: 4, name: 'David' }</span>
<span class="synIdentifier">}</span>
</pre>
<p>存在しないレコードをアップデートしようとすると、実行時エラーになってしまう。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// id が 99 のレコードの name をアップデートしようとしているが、該当するレコードは存在しないため、実行時エラーになる</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.update<span class="synStatement">(</span><span class="synIdentifier">{</span> <span class="synComment">// 実行時エラー</span>
where: <span class="synIdentifier">{</span>
id: <span class="synConstant">99</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
data: <span class="synIdentifier">{</span>
name: <span class="synConstant">"David"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<h3>レコードの削除</h3>
<p>レコードの削除には<code>delete</code>メソッドを使う。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.<span class="synStatement">delete(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
id: <span class="synConstant">4</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span> <span class="synComment">// { id: 4, name: 'David' }</span>
<span class="synIdentifier">}</span>
</pre>
<p>存在しないレコードを削除しようとすると、実行時エラーになってしまう。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// id が 99 のレコードを削除しようとしているが、該当するレコードは存在しないため、実行時エラーになる</span>
<span class="synStatement">async</span> <span class="synStatement">function</span> main<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> prisma.user.<span class="synStatement">delete(</span><span class="synIdentifier">{</span> <span class="synComment">// 実行時エラー</span>
where: <span class="synIdentifier">{</span>
id: <span class="synConstant">99</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span>user<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<h2>マイグレーション</h2>
<p>モデルの追加・変更を行う場合は、スキーマファイルを更新して、再び<code>prisma migrate</code>を行えばいい。<br />
今回は<code>User</code>モデルに<code>isAdmin</code>フィールドを足すことにする。</p>
<pre class="code prisma" data-lang="prisma" data-unlink>model User {
id Int @id @default(autoincrement())
name String
isAdmin Boolean
}</pre>
<p>しかしこの状態で<code>% yarn run prisma migrate dev --name add-is-admin</code>を実行すると、エラーになってしまう</p>
<pre class="code" data-lang="" data-unlink>Error:
⚠️ We found changes that cannot be executed:
• Step 0 Added the required column `isAdmin` to the `User` table without a default value. There are 3 rows in this table, it is not possible to execute this step.</pre>
<p><code>User</code>には<code>isAdmin</code>が必須になるように変更を行おうとしたが、既に<code>User</code>テーブルにはレコードが 3 つあり、それらは当然<code>isAdmin</code>を持っていないため、エラーになってしまう。<br />
対応方法はいくつかあるが、今回はデフォルト値を設定して解決することにする。</p>
<p>スキーマファイルを編集し、<code>isAdmin</code>にデフォルト値を設定する。</p>
<pre class="code prisma" data-lang="prisma" data-unlink>model User {
id Int @id @default(autoincrement())
name String
isAdmin Boolean @default(false)
}</pre>
<p>再度<code>% yarn run prisma migrate dev --name add-is-admin</code>を実行すると、今度は上手くいく。</p>
<p>データベースを調べてみると、新しいモデルの定義が反映されていることを確認できる。</p>
<pre class="code" data-lang="" data-unlink>mysql> DESC User;
+---------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(191) | NO | | NULL | |
| isAdmin | tinyint(1) | NO | | 0 | |
+---------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
mysql> SELECT * FROM User;
+----+-------+---------+
| id | name | isAdmin |
+----+-------+---------+
| 1 | Alice | 0 |
| 2 | Bob | 0 |
| 3 | Carol | 0 |
+----+-------+---------+
3 rows in set (0.00 sec)</pre>
<p><code>node_modules/.prisma/client/index.d.ts</code>の型定義も更新されているので、引き続き型の恩恵を受けながら開発することができる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">/**</span>
<span class="synComment"> * Model User</span>
<span class="synComment"> * </span>
<span class="synComment"> */</span>
<span class="synStatement">export</span> <span class="synStatement">type</span> User <span class="synStatement">=</span> <span class="synIdentifier">{</span>
id: <span class="synType">number</span>
name: <span class="synType">string</span>
isAdmin: <span class="synType">boolean</span>
<span class="synIdentifier">}</span>
</pre>
<h2>API サーバの構築</h2>
<p>最後に実践として、Prisma を使った簡単な API サーバを構築してみる。</p>
<p>仕様は以下のようにする。</p>
<ul>
<li><code>/user</code>に<code>GET</code>メソッドでリクエストすると、全ての<code>User</code>が返ってくる</li>
<li><code>/user/{ID}</code>に<code>GET</code>メソッドでリクエストすると、<code>id</code>フィールドが<code>ID</code>の<code>User</code>が返ってくる</li>
<li><code>/user</code>に<code>POST</code>メソッドでリクエストすると、<code>User</code>が新規作成され、その<code>User</code>が返ってくる</li>
</ul>
<p>まず、Node.js の型定義ファイルをインストールする。</p>
<pre class="code" data-lang="" data-unlink>% yarn add -D @types/node@16</pre>
<p>次に、<code>src/db.ts</code>という名前のファイルを作成する。このファイルでは、Prisma クライアントでデータベース操作を行う。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// src/db.ts</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> PrismaClient<span class="synStatement">,</span> User <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"@prisma/client"</span><span class="synStatement">;</span>
<span class="synType">const</span> prisma <span class="synStatement">=</span> <span class="synStatement">new</span> PrismaClient<span class="synStatement">();</span>
<span class="synStatement">export</span> <span class="synStatement">async</span> <span class="synStatement">function</span> getUser<span class="synStatement">(</span>id: <span class="synType">number</span><span class="synStatement">)</span>: <span class="synSpecial">Promise</span><span class="synStatement"><</span>User | <span class="synType">null</span><span class="synStatement">></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">await</span> prisma.user.findUnique<span class="synStatement">(</span><span class="synIdentifier">{</span>
where: <span class="synIdentifier">{</span>
id<span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">async</span> <span class="synStatement">function</span> getAllUsers<span class="synStatement">()</span>: <span class="synSpecial">Promise</span><span class="synStatement"><</span>User<span class="synIdentifier">[]</span><span class="synStatement">></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">await</span> prisma.user.findMany<span class="synStatement">();</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">async</span> <span class="synStatement">function</span> createUser<span class="synStatement">(</span><span class="synIdentifier">{</span> name <span class="synIdentifier">}</span>: <span class="synIdentifier">{</span> name: <span class="synType">string</span> <span class="synIdentifier">}</span><span class="synStatement">)</span>: <span class="synSpecial">Promise</span><span class="synStatement"><</span>User<span class="synStatement">></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">await</span> prisma.user.create<span class="synStatement">(</span><span class="synIdentifier">{</span>
data: <span class="synIdentifier">{</span>
name<span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<p>次に、<code>src/index.ts</code>でサーバについて記述していくのだが、まずは「全ての<code>User</code>の取得」にのみ、対応させる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> http <span class="synStatement">from</span> <span class="synConstant">"http"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> getAllUsers <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"./db"</span><span class="synStatement">;</span>
<span class="synStatement">function</span> resNotFound<span class="synStatement">(</span>res: http.ServerResponse<span class="synStatement">)</span> <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">404</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
<span class="synConstant">"Content-Type"</span>: <span class="synConstant">"application/json"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
res.write<span class="synStatement">(</span><span class="synSpecial">JSON</span>.stringify<span class="synStatement">(</span><span class="synIdentifier">{</span> message: <span class="synConstant">"Not Found"</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> + <span class="synConstant">"\n"</span><span class="synStatement">);</span>
res.end<span class="synStatement">();</span>
<span class="synIdentifier">}</span>
<span class="synStatement">function</span> resData<span class="synStatement">(</span>res: http.ServerResponse<span class="synStatement">,</span> data: <span class="synType">unknown</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">200</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
<span class="synConstant">"Content-Type"</span>: <span class="synConstant">"application/json"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
res.write<span class="synStatement">(</span><span class="synSpecial">JSON</span>.stringify<span class="synStatement">(</span>data<span class="synStatement">)</span> + <span class="synConstant">"\n"</span><span class="synStatement">);</span>
res.end<span class="synStatement">();</span>
<span class="synIdentifier">}</span>
http
.createServer<span class="synStatement">(async</span> <span class="synStatement">(</span>req<span class="synStatement">,</span> res<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>req.url <span class="synStatement">===</span> <span class="synType">undefined</span> <span class="synConstant">||</span> <span class="synConstant">!</span>req.url.startsWith<span class="synStatement">(</span><span class="synConstant">"/user"</span><span class="synStatement">))</span> <span class="synIdentifier">{</span>
resNotFound<span class="synStatement">(</span>res<span class="synStatement">);</span>
<span class="synStatement">return;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">switch</span> <span class="synStatement">(</span>req.method<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">case</span> <span class="synConstant">"GET"</span>: <span class="synIdentifier">{</span>
<span class="synStatement">switch</span> <span class="synStatement">(</span><span class="synConstant">true</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">case</span> <span class="synConstant">/^\/user$/</span>.test<span class="synStatement">(</span>req.url<span class="synStatement">)</span>: <span class="synIdentifier">{</span>
<span class="synType">const</span> allUsers <span class="synStatement">=</span> <span class="synStatement">await</span> getAllUsers<span class="synStatement">();</span>
resData<span class="synStatement">(</span>res<span class="synStatement">,</span> <span class="synIdentifier">{</span> data: allUsers <span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">default</span>: <span class="synIdentifier">{</span>
resNotFound<span class="synStatement">(</span>res<span class="synStatement">);</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">default</span>: <span class="synIdentifier">{</span>
res.writeHead<span class="synStatement">(</span><span class="synConstant">405</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
<span class="synConstant">"Content-Type"</span>: <span class="synConstant">"application/json"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
res.write<span class="synStatement">(</span><span class="synSpecial">JSON</span>.stringify<span class="synStatement">(</span><span class="synIdentifier">{</span> message: <span class="synConstant">"Method Not Allowed"</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> + <span class="synConstant">"\n"</span><span class="synStatement">);</span>
res.end<span class="synStatement">;</span>
<span class="synStatement">break;</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
.listen<span class="synStatement">(</span><span class="synConstant">8080</span><span class="synStatement">);</span>
</pre>
<p><code>% yarn run ts-node-dev src/index.ts</code>でサーバが起動するので、早速リクエストしてみる。</p>
<pre class="code" data-lang="" data-unlink>% curl http://localhost:8080/user
{"data":[{"id":1,"name":"Alice","isAdmin":false},{"id":2,"name":"Bob","isAdmin":false},{"id":3,"name":"Carol","isAdmin":false}]}</pre>
<p>問題なく全ての<code>User</code>を取得できた。</p>
<p>次に、<code>id</code>を指定して特定の<code>User</code>を取得できるようにする。</p>
<p>以下の変更を加えてサーバを起動し直す。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synStatement">@@ -1,6 +1,6 @@</span>
import http from "http";
<span class="synSpecial">-import { getAllUsers } from "./db";</span>
<span class="synIdentifier">+import { getAllUsers, getUser } from "./db";</span>
function resNotFound(res: http.ServerResponse) {
res.writeHead(404, {
<span class="synStatement">@@ -33,6 +33,12 @@</span>
resData(res, { data: allUsers });
break;
}
<span class="synIdentifier">+ case /^\/user\/[0-9]+$/.test(req.url): {</span>
<span class="synIdentifier">+ const id = Number(req.url.slice(6));</span>
<span class="synIdentifier">+ const user = await getUser(id);</span>
<span class="synIdentifier">+ resData(res, { data: user });</span>
<span class="synIdentifier">+ break;</span>
<span class="synIdentifier">+ }</span>
default: {
resNotFound(res);
break;
</pre>
<p>そうすると、個別の<code>User</code>を取得できるようになる。</p>
<pre class="code" data-lang="" data-unlink>% curl http://localhost:8080/user/1
{"data":{"id":1,"name":"Alice","isAdmin":false}}
% curl http://localhost:8080/user/2
{"data":{"id":2,"name":"Bob","isAdmin":false}}
% curl http://localhost:8080/user/9
{"data":null}</pre>
<p>最後に、<code>User</code>を作成できるようにする。<br />
以下の変更を加えてサーバを起動し直せばよい。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synStatement">@@ -1,6 +1,7 @@</span>
import http from "http";
<span class="synIdentifier">+import { URLSearchParams } from "url";</span>
<span class="synSpecial">-import { getAllUsers, getUser } from "./db";</span>
<span class="synIdentifier">+import { getAllUsers, getUser, createUser } from "./db";</span>
function resNotFound(res: http.ServerResponse) {
res.writeHead(404, {
<span class="synStatement">@@ -10,6 +11,14 @@</span>
res.end();
}
<span class="synIdentifier">+function resBadRequest(res: http.ServerResponse) {</span>
<span class="synIdentifier">+ res.writeHead(400, {</span>
<span class="synIdentifier">+ "Content-Type": "application/json",</span>
<span class="synIdentifier">+ });</span>
<span class="synIdentifier">+ res.write(JSON.stringify({ message: "Bad Request" }) + "\n");</span>
<span class="synIdentifier">+ res.end();</span>
<span class="synIdentifier">+}</span>
<span class="synIdentifier">+</span>
function resData(res: http.ServerResponse, data: unknown) {
res.writeHead(200, {
"Content-Type": "application/json",
<span class="synStatement">@@ -47,6 +56,33 @@</span>
break;
}
<span class="synIdentifier">+ case "POST": {</span>
<span class="synIdentifier">+ switch (true) {</span>
<span class="synIdentifier">+ case /^\/user$/.test(req.url): {</span>
<span class="synIdentifier">+ let data = "";</span>
<span class="synIdentifier">+ req.on("data", (chunk) => {</span>
<span class="synIdentifier">+ data += chunk;</span>
<span class="synIdentifier">+ });</span>
<span class="synIdentifier">+ req.on("end", async () => {</span>
<span class="synIdentifier">+ const params = new URLSearchParams(data);</span>
<span class="synIdentifier">+ const name = params.get("name");</span>
<span class="synIdentifier">+ if (name === null) {</span>
<span class="synIdentifier">+ resBadRequest(res);</span>
<span class="synIdentifier">+ return;</span>
<span class="synIdentifier">+ }</span>
<span class="synIdentifier">+ const user = await createUser({ name });</span>
<span class="synIdentifier">+ resData(res, { data: user });</span>
<span class="synIdentifier">+ });</span>
<span class="synIdentifier">+ break;</span>
<span class="synIdentifier">+ }</span>
<span class="synIdentifier">+ default: {</span>
<span class="synIdentifier">+ resNotFound(res);</span>
<span class="synIdentifier">+ break;</span>
<span class="synIdentifier">+ }</span>
<span class="synIdentifier">+ }</span>
<span class="synIdentifier">+ break;</span>
<span class="synIdentifier">+ }</span>
<span class="synIdentifier">+</span>
default: {
res.writeHead(405, {
"Content-Type": "application/json",
</pre>
<p><code>POST</code>メソッドで<code>User</code>を作成できるようになっている。</p>
<pre class="code" data-lang="" data-unlink>% curl -d name="Dave" http://localhost:8080/user
{"data":{"id":5,"name":"Dave","isAdmin":false}}
% curl http://localhost:8080/user
{"data":[{"id":1,"name":"Alice","isAdmin":false},{"id":2,"name":"Bob","isAdmin":false},{"id":3,"name":"Carol","isAdmin":false},{"id":5,"name":"Dave","isAdmin":false}]}</pre>
<p>今回は対応しなかったが、アップデートや削除も同じ要領で実装すればよい。</p>
numb_86
Next.js で始める GraphQL
hatenablog://entry/13574176438072992192
2022-03-14T20:37:23+09:00
2022-03-14T20:37:23+09:00 この記事では、GraphQL を利用したアプリを Next.js で構築していきながら、GraphQL の初歩について書いていく。 GraphQL のクライアントもサーバも、Apollo を用いる。 また、できるだけ型安全に開発したいので、graphql-codegenで型定義ファイルを生成する方法も扱う。 利用しているライブラリのバージョンは以下の通り。 @apollo/client@3.5.10 @graphql-codegen/cli@2.6.2 @graphql-codegen/typed-document-node@2.2.7 @graphql-codegen/typescript-…
<p>この記事では、GraphQL を利用したアプリを Next.js で構築していきながら、GraphQL の初歩について書いていく。</p>
<p>GraphQL のクライアントもサーバも、Apollo を用いる。<br />
また、できるだけ型安全に開発したいので、<code>graphql-codegen</code>で型定義ファイルを生成する方法も扱う。</p>
<p>利用しているライブラリのバージョンは以下の通り。</p>
<ul>
<li>@apollo/client@3.5.10</li>
<li>@graphql-codegen/cli@2.6.2</li>
<li>@graphql-codegen/typed-document-node@2.2.7</li>
<li>@graphql-codegen/typescript-operations@2.3.4</li>
<li>@graphql-codegen/typescript-resolvers@2.5.4</li>
<li>@graphql-codegen/typescript@2.4.7</li>
<li>@types/node@17.0.21</li>
<li>@types/react@17.0.40</li>
<li>apollo-server-micro@3.6.4</li>
<li>concurrently@7.0.0</li>
<li>eslint-config-next@12.1.0</li>
<li>eslint@8.11.0</li>
<li>graphql@16.3.0</li>
<li>micro@9.3.4</li>
<li>next@12.1.0</li>
<li>react-dom@17.0.2</li>
<li>react@17.0.2</li>
<li>typescript@4.6.2</li>
</ul>
<h2>スキーマを定義する</h2>
<p>まず、Next.js の環境を構築する。</p>
<pre class="code" data-lang="" data-unlink>$ yarn create next-app sample --ts</pre>
<p>プロジェクトのルートディレクトリ(今回の例だと<code>sample</code>)に<code>graphql</code>ディレクトリを作り、そのなかに以下の内容の<code>schema.graphql</code>を作る。</p>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synType">type</span> <span class="synType">User</span> <span class="synSpecial">{</span>
<span class="synIdentifier">id</span>: <span class="synType">ID</span><span class="synStatement">!</span>
<span class="synIdentifier">name</span>: <span class="synType">String</span><span class="synStatement">!</span>
<span class="synSpecial">}</span>
</pre>
<p><code>User</code>というデータ構造を定義している。<code>User</code>は<code>id</code>と<code>name</code>を持つ。末尾の<code>!</code>は<code>null</code>を許容しないことを意味している。</p>
<p>次に、クライアントがこの<code>User</code>を取得するためのクエリのスキーマを定義する。<br />
<code>schema.graphql</code>に以下を追記する。</p>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synType">type</span> <span class="synType">Query</span> <span class="synSpecial">{</span>
<span class="synIdentifier">users</span>: <span class="synSpecial">[</span><span class="synType">User</span><span class="synStatement">!</span><span class="synSpecial">]</span><span class="synStatement">!</span>
<span class="synSpecial">}</span>
</pre>
<p><code>users</code>というクエリを定義しており、このクエリを使うとクライアントは<code>User</code>の配列を得ることができる。</p>
<h2>GraphQL サーバを構築する</h2>
<p>今回は Next.js の API Routes を GraphQL サーバとして使うことにする。<br />
そのため<code>pages/api</code>以下にファイルを作成することになるが、その前にまず、先程定義したスキーマに基づいて型定義ファイルを生成する。</p>
<p>ファイル生成に必要なライブラリをインストール。</p>
<pre class="code" data-lang="" data-unlink>$ yarn add graphql
$ yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers</pre>
<p>続いて、以下の内容の<code>graphql/codegen-server.yaml</code>を作る。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">schema</span><span class="synSpecial">:</span> <span class="synConstant">'./graphql/schema.graphql'</span>
<span class="synIdentifier">generates</span><span class="synSpecial">:</span>
<span class="synIdentifier">./graphql/dist/generated-server.ts</span><span class="synSpecial">:</span>
<span class="synIdentifier">config</span><span class="synSpecial">:</span>
<span class="synIdentifier">useIndexSignature</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synIdentifier">plugins</span><span class="synSpecial">:</span>
<span class="synStatement">- </span>typescript
<span class="synStatement">- </span>typescript-resolvers
</pre>
<p>この状態で以下のコマンドを実行すると、<code>graphql/dist/generated-server.ts</code>が生成される。</p>
<pre class="code" data-lang="" data-unlink>$ yarn run graphql-codegen --config graphql/codegen-server.yaml</pre>
<p>このファイルを使うことで、型の恩恵を受けながら GraphQL サーバを開発することができる。</p>
<p>今回は<code>micro</code>を使って GraphQL サーバを構築するので、そのために必要なライブラリをインストールする。</p>
<pre class="code" data-lang="" data-unlink>$ yarn add micro apollo-server-micro</pre>
<p>以下の内容の<code>pages/api/graphql.ts</code>を作成する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> NextApiRequest<span class="synStatement">,</span> NextApiResponse <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"next"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> ApolloServer <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"apollo-server-micro"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> readFileSync <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"fs"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> join <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"path"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> Resolvers <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"../../graphql/dist/generated-server"</span><span class="synStatement">;</span>
<span class="synType">const</span> path <span class="synStatement">=</span> join<span class="synStatement">(</span><span class="synSpecial">process</span>.cwd<span class="synStatement">(),</span> <span class="synConstant">"graphql"</span><span class="synStatement">,</span> <span class="synConstant">"schema.graphql"</span><span class="synStatement">);</span>
<span class="synType">const</span> typeDefs <span class="synStatement">=</span> readFileSync<span class="synStatement">(</span>path<span class="synStatement">)</span>.toString<span class="synStatement">(</span><span class="synConstant">"utf-8"</span><span class="synStatement">);</span>
<span class="synComment">// スキーマと実際のデータ構造の紐付けを resolvers で行う</span>
<span class="synType">const</span> resolvers: Resolvers <span class="synStatement">=</span> <span class="synIdentifier">{}</span><span class="synStatement">;</span>
<span class="synType">const</span> apolloServer <span class="synStatement">=</span> <span class="synStatement">new</span> ApolloServer<span class="synStatement">(</span><span class="synIdentifier">{</span> typeDefs<span class="synStatement">,</span> resolvers <span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> startServer <span class="synStatement">=</span> apolloServer.start<span class="synStatement">();</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synStatement">async</span> <span class="synStatement">function</span> handler<span class="synStatement">(</span>
req: NextApiRequest<span class="synStatement">,</span>
res: NextApiResponse
<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">await</span> startServer<span class="synStatement">;</span>
<span class="synStatement">await</span> apolloServer.createHandler<span class="synStatement">(</span><span class="synIdentifier">{</span>
path: <span class="synConstant">"/api/graphql"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">)(</span>req<span class="synStatement">,</span> res<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synType">const</span> config <span class="synStatement">=</span> <span class="synIdentifier">{</span>
api: <span class="synIdentifier">{</span>
bodyParser: <span class="synConstant">false</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<p>スキーマ定義の際に<code>users</code>というクエリを定義したが、実際にどのようなロジックでどのようなデータを返すのかは、まだどこにも定義されていない。<br />
それを定義するのがリゾルバである。<br />
今回は<code>pages/api/graphql.ts</code>にデータをハードコーディングするが、データベースから取得してもいいし、他のサービスの API から取得してもいい。</p>
<p>早速書いてみる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> resolvers: Resolvers <span class="synStatement">=</span> <span class="synIdentifier">{</span>
Query: <span class="synIdentifier">{</span>
users: <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">[{</span> id: <span class="synConstant">"1"</span> <span class="synIdentifier">}]</span><span class="synStatement">,</span> <span class="synComment">// Type Error</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<p>上記のように書くと型エラーが出るが、これは<code>name</code>が含まれていないため。<br />
自動生成された<code>Resolvers</code>を利用しているため、このように型の恩恵を受けることができる。<br />
同様に、<code>name</code>を<code>true</code>にするなど、型が間違っていてもエラーになる。<br />
ただ、以下のように余計なプロパティがついていても型エラーにはならないので注意する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> resolvers: Resolvers <span class="synStatement">=</span> <span class="synIdentifier">{</span>
Query: <span class="synIdentifier">{</span>
users: <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">[{</span> id: <span class="synConstant">"1"</span><span class="synStatement">,</span> name: <span class="synConstant">"Alice"</span><span class="synStatement">,</span> foo: <span class="synConstant">"bar"</span> <span class="synIdentifier">}]</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<p>今回は以下のようにした。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> users <span class="synStatement">=</span> <span class="synIdentifier">[</span>
<span class="synIdentifier">{</span> id: <span class="synConstant">"1"</span><span class="synStatement">,</span> name: <span class="synConstant">"Alice"</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">{</span> id: <span class="synConstant">"2"</span><span class="synStatement">,</span> name: <span class="synConstant">"Bob"</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">{</span> id: <span class="synConstant">"3"</span><span class="synStatement">,</span> name: <span class="synConstant">"Carol"</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">]</span><span class="synStatement">;</span>
<span class="synType">const</span> resolvers: Resolvers <span class="synStatement">=</span> <span class="synIdentifier">{</span>
Query: <span class="synIdentifier">{</span>
users: <span class="synStatement">()</span> <span class="synStatement">=></span> users<span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<p>これでサーバの構築は完了したので、クライアントの構築に移る。</p>
<h2>GraphQL クライアントを構築する</h2>
<p>サーバと同様、まずは型定義ファイルを生成するための作業を行う。</p>
<p>以下の内容の<code>graphql/query.graphql</code>を作る。</p>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synType">query</span> <span class="synIdentifier">getUsersName</span> <span class="synSpecial">{</span>
<span class="synIdentifier">users</span> <span class="synSpecial">{</span>
<span class="synIdentifier">name</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p>GraphQL では、どのようなデータを取得するのかをクライアント側が決める。<br />
上記の例では、<code>users</code>クエリを使って<code>name</code>のみを取得している。<code>id</code>は不要と判断し、取得していない。</p>
<p>コード生成のためのライブラリをインストール。</p>
<pre class="code" data-lang="" data-unlink>$ yarn add -D @graphql-codegen/typed-document-node @graphql-codegen/typescript-operations</pre>
<p><code>graphql/codegen-client.yaml</code>を作る。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">schema</span><span class="synSpecial">:</span> <span class="synConstant">'./graphql/schema.graphql'</span>
<span class="synIdentifier">documents</span><span class="synSpecial">:</span> <span class="synConstant">'./graphql/*.graphql'</span>
<span class="synIdentifier">generates</span><span class="synSpecial">:</span>
<span class="synIdentifier">./graphql/dist/generated-client.ts</span><span class="synSpecial">:</span>
<span class="synIdentifier">plugins</span><span class="synSpecial">:</span>
<span class="synStatement">- </span>typescript
<span class="synStatement">- </span>typescript-operations
<span class="synStatement">- </span>typed-document-node
</pre>
<p>この状態で以下のコマンドを実行すると、<code>graphql/dist/generated-client.ts</code>が生成される。</p>
<pre class="code" data-lang="" data-unlink>$ yarn run graphql-codegen --config graphql/codegen-client.yaml</pre>
<p>これで型定義ファイルが生成されたので、実際に GraphQL クライアントを構築していく。<br />
サーバと同様に Apollo を使うので、インストールする。</p>
<pre class="code" data-lang="" data-unlink>$ yarn add @apollo/client</pre>
<p>そして、<code>pages/index.tsx</code>を以下のように書き換える。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span>
ApolloClient<span class="synStatement">,</span>
InMemoryCache<span class="synStatement">,</span>
ApolloProvider<span class="synStatement">,</span>
useQuery<span class="synStatement">,</span>
<span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"@apollo/client"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> GetUsersNameDocument <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"../graphql/dist/generated-client"</span><span class="synStatement">;</span>
<span class="synType">const</span> client <span class="synStatement">=</span> <span class="synStatement">new</span> ApolloClient<span class="synStatement">(</span><span class="synIdentifier">{</span>
uri: <span class="synConstant">"http://localhost:3000/api/graphql"</span><span class="synStatement">,</span>
cache: <span class="synStatement">new</span> InMemoryCache<span class="synStatement">(),</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">function</span> Users<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> loading<span class="synStatement">,</span> error<span class="synStatement">,</span> data <span class="synIdentifier">}</span> <span class="synStatement">=</span> useQuery<span class="synStatement">(</span>GetUsersNameDocument<span class="synStatement">);</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>loading<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span>Loading...<span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>error <span class="synConstant">||</span> <span class="synConstant">!</span>data<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span><span class="synSpecial">Error</span><span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>ul<span class="synStatement">></span>
<span class="synIdentifier">{</span>data.users.map<span class="synStatement">((</span>user<span class="synStatement">,</span> index: <span class="synType">number</span><span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synSpecial">Object</span>.getOwnPropertyNames<span class="synStatement">(</span>user<span class="synStatement">));</span> <span class="synComment">// ['__typename', 'name']</span>
<span class="synStatement">return</span> <span class="synStatement"><</span>li key<span class="synStatement">=</span><span class="synIdentifier">{</span>index<span class="synIdentifier">}</span><span class="synStatement">></span><span class="synIdentifier">{</span>user.name<span class="synIdentifier">}</span><span class="synStatement"><</span>/li<span class="synStatement">>;</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span>
<span class="synStatement"><</span>/ul<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synStatement">function</span> App<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>ApolloProvider client<span class="synStatement">=</span><span class="synIdentifier">{</span>client<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>Users /<span class="synStatement">></span>
<span class="synStatement"><</span>/ApolloProvider<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<p><code>$ yarn dev</code>を実行して<code>http://localhost:3000/</code>にアクセスすると、ユーザー名のリストが表示されているはず。<br />
そして、<code>Object.getOwnPropertyNames(user)</code>の結果を見ると分かるように、<code>id</code>は取得されておらず、指定したデータのみを取得できていることが分かる。</p>
<h2>watch オプションを使う</h2>
<p>今の状態だと、スキーマ定義などが更新される度に、コマンドを実行して型定義ファイルを生成しないといけない。<br />
<code>graphql-codegen</code>の<code>watch</code>オプションを使うことで、ファイル更新時に自動的に再生成されるようになる。</p>
<pre class="code" data-lang="" data-unlink>$ yarn run graphql-codegen --config graphql/codegen-server.yaml --watch
$ yarn run graphql-codegen --config graphql/codegen-client.yaml --watch</pre>
<p>開発環境の起動時にこれらのコマンドを実行すると便利なので、<code>npm scripts</code>を使って実現する。</p>
<p>まず、複数の npm script を実行させたいので、<code>concurrently</code>をインストールする。</p>
<pre class="code" data-lang="" data-unlink>$ yarn add -D concurrently</pre>
<p>次に、<code>package.json</code>の<code>scripts</code>フィールドを以下のように書き換える。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">scripts</span>": <span class="synSpecial">{</span>
"<span class="synStatement">dev</span>": "<span class="synConstant">concurrently </span><span class="synSpecial">\"</span><span class="synConstant">yarn run generate-client --watch</span><span class="synSpecial">\"</span><span class="synConstant"> </span><span class="synSpecial">\"</span><span class="synConstant">yarn run generate-server --watch</span><span class="synSpecial">\"</span><span class="synConstant"> </span><span class="synSpecial">\"</span><span class="synConstant">next dev</span><span class="synSpecial">\"</span>",
"<span class="synStatement">build</span>": "<span class="synConstant">next build</span>",
"<span class="synStatement">start</span>": "<span class="synConstant">next start</span>",
"<span class="synStatement">lint</span>": "<span class="synConstant">next lint</span>",
"<span class="synStatement">generate-client</span>": "<span class="synConstant">graphql-codegen --config graphql/codegen-server.yaml</span>",
"<span class="synStatement">generate-server</span>": "<span class="synConstant">graphql-codegen --config graphql/codegen-client.yaml</span>"
<span class="synSpecial">}</span>,
</pre>
<p>この状態で<code>$ yarn dev</code>を実行すると、スキーマ定義やクエリを更新した際に、型定義ファイルも自動的に再生成される。</p>
<p>例えば、<code>graphql/query.graphql</code>を以下のように書き換えると、<code>graphql/dist/generated-client</code>も自動的に再生成される。</p>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synType">query</span> <span class="synIdentifier">getUsers</span> <span class="synSpecial">{</span>
<span class="synIdentifier">users</span> <span class="synSpecial">{</span>
<span class="synIdentifier">id</span>
<span class="synIdentifier">name</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p>そのため、<code>pages/index.tsx</code>の以下の部分がエラーになる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> GetUsersNameDocument <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"../graphql/dist/generated-client"</span><span class="synStatement">;</span> <span class="synComment">// Error</span>
</pre>
<p><code>GetUsersNameDocument</code>を<code>GetUsersDocument</code>に置換すると解決すると同時に、取得した<code>user</code>のなかに<code>id</code>が含まれるようになる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synSpecial">Object</span>.getOwnPropertyNames<span class="synStatement">(</span>user<span class="synStatement">));</span> <span class="synComment">// ['__typename', 'id', 'name']</span>
</pre>
<p>これでアプリの構築は終わったが、このアプリを題材にして、GraphQL を使い方をもう少し見ていく。</p>
<h2>一度のリクエストで複数のクエリを利用する</h2>
<p>GraphQL のメリットとしてよく語られることのひとつに、一度のリクエストで必要なデータを取得できる、というものがある。<br />
例として、<code>User</code>だけでなく<code>Team</code>というデータ構造も使うことになったケースについて考えてみる。</p>
<p>まず、スキーマに<code>Team</code>を追加する。</p>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synComment"># graphql/schema.graphql</span>
<span class="synType">type</span> <span class="synType">User</span> <span class="synSpecial">{</span>
<span class="synIdentifier">id</span>: <span class="synType">ID</span><span class="synStatement">!</span>
<span class="synIdentifier">name</span>: <span class="synType">String</span><span class="synStatement">!</span>
<span class="synIdentifier">teamName</span>: <span class="synType">String</span><span class="synStatement">!</span>
<span class="synSpecial">}</span>
<span class="synType">type</span> <span class="synType">Team</span> <span class="synSpecial">{</span>
<span class="synIdentifier">id</span>: <span class="synType">ID</span><span class="synStatement">!</span>
<span class="synIdentifier">name</span>: <span class="synType">String</span><span class="synStatement">!</span>
<span class="synSpecial">}</span>
<span class="synType">type</span> <span class="synType">Query</span> <span class="synSpecial">{</span>
<span class="synIdentifier">users</span>: <span class="synSpecial">[</span><span class="synType">User</span><span class="synStatement">!</span><span class="synSpecial">]</span><span class="synStatement">!</span>
<span class="synIdentifier">teams</span>: <span class="synSpecial">[</span><span class="synType">Team</span><span class="synStatement">!</span><span class="synSpecial">]</span><span class="synStatement">!</span>
<span class="synSpecial">}</span>
</pre>
<p><code>pages/api/graphql.ts</code>に型エラーが出ているはずなので、新しいスキーマ定義に合わせて修正する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> Team <span class="synStatement">=</span> <span class="synConstant">"Red"</span> | <span class="synConstant">"White"</span><span class="synStatement">;</span>
<span class="synType">const</span> teams: <span class="synIdentifier">{</span> id: <span class="synType">string</span><span class="synStatement">;</span> name: Team <span class="synIdentifier">}[]</span> <span class="synStatement">=</span> <span class="synIdentifier">[</span>
<span class="synIdentifier">{</span> id: <span class="synConstant">"1"</span><span class="synStatement">,</span> name: <span class="synConstant">"Red"</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">{</span> id: <span class="synConstant">"2"</span><span class="synStatement">,</span> name: <span class="synConstant">"White"</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">]</span><span class="synStatement">;</span>
<span class="synStatement">type</span> User <span class="synStatement">=</span> <span class="synIdentifier">{</span> id: <span class="synType">string</span><span class="synStatement">;</span> name: <span class="synType">string</span><span class="synStatement">;</span> teamName: Team <span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> users: User<span class="synIdentifier">[]</span> <span class="synStatement">=</span> <span class="synIdentifier">[</span>
<span class="synIdentifier">{</span> id: <span class="synConstant">"1"</span><span class="synStatement">,</span> name: <span class="synConstant">"Alice"</span><span class="synStatement">,</span> teamName: <span class="synConstant">"Red"</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">{</span> id: <span class="synConstant">"2"</span><span class="synStatement">,</span> name: <span class="synConstant">"Bob"</span><span class="synStatement">,</span> teamName: <span class="synConstant">"Red"</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">{</span> id: <span class="synConstant">"3"</span><span class="synStatement">,</span> name: <span class="synConstant">"Carol"</span><span class="synStatement">,</span> teamName: <span class="synConstant">"White"</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">]</span><span class="synStatement">;</span>
<span class="synType">const</span> resolvers: Resolvers <span class="synStatement">=</span> <span class="synIdentifier">{</span>
Query: <span class="synIdentifier">{</span>
users: <span class="synStatement">()</span> <span class="synStatement">=></span> users<span class="synStatement">,</span>
teams: <span class="synStatement">()</span> <span class="synStatement">=></span> teams<span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<p>次に<code>graphql/query.graphql</code>にクエリを書いていく。<br />
この際、<code>getUsers</code>と同じ要領で<code>getTeams</code>を書いてその 2 つを使ってもいいのだが、ひとつのクエリでまとめて<code>User</code>と<code>Team</code>を取得することができる。</p>
<p>具体的には、<code>graphql/query.graphql</code>に以下の内容を追記すればよい。</p>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synType">query</span> <span class="synIdentifier">getUsersAndTeams</span> <span class="synSpecial">{</span>
<span class="synIdentifier">users</span> <span class="synSpecial">{</span>
<span class="synIdentifier">id</span>
<span class="synIdentifier">name</span>
<span class="synIdentifier">teamName</span>
<span class="synSpecial">}</span>
<span class="synIdentifier">teams</span> <span class="synSpecial">{</span>
<span class="synIdentifier">id</span>
<span class="synIdentifier">name</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p>そうするとクライアントで<code>GetUsersAndTeamsDocument</code>を使えるようになっているので、<code>pages/index.tsx</code>を以下のように書き換える。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span>
ApolloClient<span class="synStatement">,</span>
InMemoryCache<span class="synStatement">,</span>
ApolloProvider<span class="synStatement">,</span>
useQuery<span class="synStatement">,</span>
<span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"@apollo/client"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> GetUsersAndTeamsDocument <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"../graphql/dist/generated-client"</span><span class="synStatement">;</span>
<span class="synType">const</span> client <span class="synStatement">=</span> <span class="synStatement">new</span> ApolloClient<span class="synStatement">(</span><span class="synIdentifier">{</span>
uri: <span class="synConstant">"http://localhost:3000/api/graphql"</span><span class="synStatement">,</span>
cache: <span class="synStatement">new</span> InMemoryCache<span class="synStatement">(),</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">function</span> UsersAndTeams<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> loading<span class="synStatement">,</span> error<span class="synStatement">,</span> data <span class="synIdentifier">}</span> <span class="synStatement">=</span> useQuery<span class="synStatement">(</span>GetUsersAndTeamsDocument<span class="synStatement">);</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>loading<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span>Loading...<span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>error <span class="synConstant">||</span> <span class="synConstant">!</span>data<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span><span class="synSpecial">Error</span><span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> users<span class="synStatement">,</span> teams <span class="synIdentifier">}</span> <span class="synStatement">=</span> data<span class="synStatement">;</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><></span>
<span class="synStatement"><</span>h1<span class="synStatement">></span>Team List<span class="synStatement"><</span>/h1<span class="synStatement">></span>
<span class="synStatement"><</span>ul<span class="synStatement">></span>
<span class="synIdentifier">{</span>teams.map<span class="synStatement">((</span><span class="synIdentifier">{</span> id<span class="synStatement">,</span> name <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement"><</span>li key<span class="synStatement">=</span><span class="synIdentifier">{</span>id<span class="synIdentifier">}</span><span class="synStatement">></span><span class="synIdentifier">{</span>name<span class="synIdentifier">}</span><span class="synStatement"><</span>/li<span class="synStatement">>;</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span>
<span class="synStatement"><</span>/ul<span class="synStatement">></span>
<span class="synStatement"><</span>h1<span class="synStatement">></span>User List<span class="synStatement"><</span>/h1<span class="synStatement">></span>
<span class="synStatement"><</span>ul<span class="synStatement">></span>
<span class="synIdentifier">{</span>users.map<span class="synStatement">((</span><span class="synIdentifier">{</span> id<span class="synStatement">,</span> name<span class="synStatement">,</span> teamName <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>li key<span class="synStatement">=</span><span class="synIdentifier">{</span>id<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>b<span class="synStatement">></span><span class="synIdentifier">{</span>name<span class="synIdentifier">}</span><span class="synStatement"><</span>/b<span class="synStatement">></span> belong <span class="synIdentifier">{</span>teamName<span class="synIdentifier">}</span> team
<span class="synStatement"><</span>/li<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span>
<span class="synStatement"><</span>/ul<span class="synStatement">></span>
<span class="synStatement"><</span>/<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synStatement">function</span> App<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>ApolloProvider client<span class="synStatement">=</span><span class="synIdentifier">{</span>client<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>UsersAndTeams /<span class="synStatement">></span>
<span class="synStatement"><</span>/ApolloProvider<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<p>一度のリクエストで<code>User</code>と<code>Team</code>の両方を取得できていることが分かる。</p>
<h2>クエリ引数</h2>
<p>クエリ引数を使うことで、特定の条件を満たしたデータのみを取得することもできる。</p>
<p>例として、スキーマ定義に<code>user</code>というクエリを追加する。<br />
引数として<code>name</code>を受け取り、それに基づいて特定の<code>User</code>を返す。該当する<code>User</code>が存在しないケースがあり得るので、<code>!</code>はつけていない。</p>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synComment"># graphql/schema.graphql</span>
<span class="synType">type</span> <span class="synType">Query</span> <span class="synSpecial">{</span>
<span class="synIdentifier">users</span>: <span class="synSpecial">[</span><span class="synType">User</span><span class="synStatement">!</span><span class="synSpecial">]</span><span class="synStatement">!</span>
<span class="synIdentifier">teams</span>: <span class="synSpecial">[</span><span class="synType">Team</span><span class="synStatement">!</span><span class="synSpecial">]</span><span class="synStatement">!</span>
<span class="synIdentifier">user</span>(<span class="synIdentifier">name</span>: <span class="synType">String</span><span class="synStatement">!</span>): <span class="synType">User</span>
<span class="synSpecial">}</span>
</pre>
<p><code>pages/api/graphql.ts</code>のリゾルバに、以下の<code>user</code>メソッドを追加する。<br />
実装をみれば分かるが、渡された<code>name</code>と一致するユーザーが存在した場合はそのユーザーを、存在しなかった場合は<code>null</code>を返す。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink> user: <span class="synStatement">(</span>_<span class="synStatement">,</span> <span class="synIdentifier">{</span> name: specifiedName <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> users.find<span class="synStatement">((</span><span class="synIdentifier">{</span> name <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">=></span> name <span class="synStatement">===</span> specifiedName<span class="synStatement">);</span>
<span class="synStatement">return</span> user <span class="synConstant">||</span> <span class="synType">null</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
</pre>
<p>クライアントが使用するクエリを定義する。</p>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synComment"># graphql/query.graphql</span>
<span class="synType">query</span> <span class="synIdentifier">getUser</span>($<span class="synIdentifier">name</span>: <span class="synType">String</span><span class="synStatement">!</span>) <span class="synSpecial">{</span>
<span class="synIdentifier">user</span>(<span class="synIdentifier">name</span>: $<span class="synIdentifier">name</span>) <span class="synSpecial">{</span>
<span class="synIdentifier">id</span>
<span class="synIdentifier">name</span>
<span class="synIdentifier">teamName</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p><code>GetUserDocument</code>が使えるようになるので、<code>pages/index.tsx</code>を以下のように書き換える。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span>
ApolloClient<span class="synStatement">,</span>
InMemoryCache<span class="synStatement">,</span>
ApolloProvider<span class="synStatement">,</span>
useQuery<span class="synStatement">,</span>
<span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"@apollo/client"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> useState<span class="synStatement">,</span> memo <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"react"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> GetUserDocument <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"../graphql/dist/generated-client"</span><span class="synStatement">;</span>
<span class="synType">const</span> client <span class="synStatement">=</span> <span class="synStatement">new</span> ApolloClient<span class="synStatement">(</span><span class="synIdentifier">{</span>
uri: <span class="synConstant">"http://localhost:3000/api/graphql"</span><span class="synStatement">,</span>
cache: <span class="synStatement">new</span> InMemoryCache<span class="synStatement">(),</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> User <span class="synStatement">=</span> memo<span class="synStatement">(function</span> User<span class="synStatement">(</span><span class="synIdentifier">{</span> specifiedName <span class="synIdentifier">}</span>: <span class="synIdentifier">{</span> specifiedName: <span class="synType">string</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> loading<span class="synStatement">,</span> error<span class="synStatement">,</span> data <span class="synIdentifier">}</span> <span class="synStatement">=</span> useQuery<span class="synStatement">(</span>GetUserDocument<span class="synStatement">,</span> <span class="synIdentifier">{</span>
variables: <span class="synIdentifier">{</span> name: specifiedName <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>loading<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span>Loading...<span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>error <span class="synConstant">||</span> <span class="synConstant">!</span>data<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span><span class="synSpecial">Error</span><span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> user <span class="synIdentifier">}</span> <span class="synStatement">=</span> data<span class="synStatement">;</span>
<span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>user<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement"><</span>div<span class="synStatement">></span>user not found.<span class="synStatement"><</span>/div<span class="synStatement">>;</span>
<span class="synIdentifier">}</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> id<span class="synStatement">,</span> name<span class="synStatement">,</span> teamName <span class="synIdentifier">}</span> <span class="synStatement">=</span> user<span class="synStatement">;</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>div<span class="synStatement">></span>
<span class="synStatement"><</span>span<span class="synStatement">></span>id: <span class="synIdentifier">{</span>id<span class="synIdentifier">}</span><span class="synStatement"><</span>/span<span class="synStatement">></span>
<span class="synStatement"><</span>br /<span class="synStatement">></span>
<span class="synStatement"><</span>span<span class="synStatement">></span>name: <span class="synIdentifier">{</span>name<span class="synIdentifier">}</span><span class="synStatement"><</span>/span<span class="synStatement">></span>
<span class="synStatement"><</span>br /<span class="synStatement">></span>
<span class="synStatement"><</span>span<span class="synStatement">></span>team <span class="synIdentifier">{</span>teamName<span class="synIdentifier">}</span><span class="synStatement"><</span>/span<span class="synStatement">></span>
<span class="synStatement"><</span>/div<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">function</span> SearchUser<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>text<span class="synStatement">,</span> setText<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement">(</span><span class="synConstant">""</span><span class="synStatement">);</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>specifiedName<span class="synStatement">,</span> setSpecifiedName<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement">(</span><span class="synConstant">""</span><span class="synStatement">);</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><></span>
<span class="synStatement"><</span>h1<span class="synStatement">></span>Search User<span class="synStatement"><</span>/h1<span class="synStatement">></span>
<span class="synStatement"><</span>form
<span class="synSpecial">onSubmit</span><span class="synStatement">=</span><span class="synIdentifier">{</span><span class="synStatement">(</span>e<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
e.preventDefault<span class="synStatement">();</span>
setSpecifiedName<span class="synStatement">(</span>text<span class="synStatement">);</span>
<span class="synIdentifier">}}</span>
<span class="synStatement">></span>
<span class="synStatement"><</span>input
<span class="synStatement">type=</span><span class="synConstant">"text"</span>
value<span class="synStatement">=</span><span class="synIdentifier">{</span>text<span class="synIdentifier">}</span>
onChange<span class="synStatement">=</span><span class="synIdentifier">{</span><span class="synStatement">(</span>e<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
setText<span class="synStatement">(</span>e.currentTarget.value<span class="synStatement">);</span>
<span class="synIdentifier">}}</span>
/<span class="synStatement">></span>
<span class="synStatement"><</span>/form<span class="synStatement">></span>
<span class="synStatement"><</span>User specifiedName<span class="synStatement">=</span><span class="synIdentifier">{</span>specifiedName<span class="synIdentifier">}</span> /<span class="synStatement">></span>
<span class="synStatement"><</span>/<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synStatement">function</span> App<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>ApolloProvider client<span class="synStatement">=</span><span class="synIdentifier">{</span>client<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>SearchUser /<span class="synStatement">></span>
<span class="synStatement"><</span>/ApolloProvider<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<p>テキストボックスにユーザー名を入力して送信(エンター)すると、その結果が表示されるはず。</p>
<h2>Mutation</h2>
<p>ここまで見てきた内容は全て、GraphQL サーバからデータを取得するものだった。<br />
最後に、データを操作する方法を紹介する。</p>
<p>データの取得には<code>Query</code>を使ってきたが、データの操作には<code>Mutation</code>を使う。</p>
<p>スキーマ定義、リゾルバ定義、クライアントが使うクエリを書く、クライアントの実装、という流れはこれまでと変わらない。</p>
<h3>スキーマ定義</h3>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synComment"># graphql/schema.graphql に以下を追記</span>
<span class="synType">type</span> <span class="synType">Mutation</span> <span class="synSpecial">{</span>
<span class="synIdentifier">addUser</span>(<span class="synIdentifier">name</span>: <span class="synType">String</span><span class="synStatement">!</span>): <span class="synType">User</span>
<span class="synSpecial">}</span>
</pre>
<h3>リゾルバ定義</h3>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// pages/api/graphql.ts のリゾルバを更新</span>
<span class="synType">const</span> addUser <span class="synStatement">=</span> <span class="synStatement">(</span>newUserName: <span class="synType">string</span><span class="synStatement">)</span>: User | <span class="synType">null</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> user <span class="synStatement">=</span> users.find<span class="synStatement">((</span><span class="synIdentifier">{</span> name <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">=></span> name <span class="synStatement">===</span> newUserName<span class="synStatement">);</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>user<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synType">null</span><span class="synStatement">;</span> <span class="synComment">// 同名のユーザーが存在した場合は null を返す</span>
<span class="synType">const</span> id <span class="synStatement">=</span> users.length + <span class="synConstant">1</span><span class="synStatement">;</span>
<span class="synType">const</span> newUser: User <span class="synStatement">=</span> <span class="synIdentifier">{</span>
id: <span class="synSpecial">String</span><span class="synStatement">(</span>id<span class="synStatement">),</span>
name: newUserName<span class="synStatement">,</span>
teamName: <span class="synConstant">"White"</span><span class="synStatement">,</span> <span class="synComment">// 実装が面倒なのでチームは必ず White にするようにした</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
users.push<span class="synStatement">(</span>newUser<span class="synStatement">);</span>
<span class="synStatement">return</span> newUser<span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synType">const</span> resolvers: Resolvers <span class="synStatement">=</span> <span class="synIdentifier">{</span>
Query: <span class="synIdentifier">{</span>
<span class="synComment">// 省略</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
Mutation: <span class="synIdentifier">{</span>
addUser: <span class="synStatement">(</span>_<span class="synStatement">,</span> <span class="synIdentifier">{</span> name <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">=></span> addUser<span class="synStatement">(</span>name<span class="synStatement">),</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<h3>クエリを書く</h3>
<pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synComment"># graphql/query.graphql に以下を追記</span>
<span class="synType">mutation</span> <span class="synIdentifier">addUser</span>($<span class="synIdentifier">name</span>: <span class="synType">String</span><span class="synStatement">!</span>) <span class="synSpecial">{</span>
<span class="synIdentifier">addUser</span>(<span class="synIdentifier">name</span>: $<span class="synIdentifier">name</span>) <span class="synSpecial">{</span>
<span class="synIdentifier">id</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<h3>クライアントの実装</h3>
<p><code>pages/index.tsx</code>に<code>AddUser</code>を追加すれば完成。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> AddUser<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>text<span class="synStatement">,</span> setText<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement">(</span><span class="synConstant">""</span><span class="synStatement">);</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>addUser<span class="synStatement">,</span> <span class="synIdentifier">{</span> loading<span class="synStatement">,</span> error<span class="synStatement">,</span> data <span class="synIdentifier">}]</span> <span class="synStatement">=</span> useMutation<span class="synStatement">(</span>AddUserDocument<span class="synStatement">);</span>
<span class="synType">const</span> Status <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>loading<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span>Loading...<span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>error<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span><span class="synSpecial">Error</span><span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>data <span class="synConstant">&&</span> <span class="synConstant">!</span>data.addUser<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span>同名のユーザーが既に存在します<span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>data<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synType">null</span><span class="synStatement">;</span>
<span class="synStatement">return</span> <span class="synStatement"><</span>p<span class="synStatement">></span>登録が完了しました<span class="synStatement"><</span>/p<span class="synStatement">>;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><></span>
<span class="synStatement"><</span>h1<span class="synStatement">></span>Add User<span class="synStatement"><</span>/h1<span class="synStatement">></span>
<span class="synStatement"><</span>form
<span class="synSpecial">onSubmit</span><span class="synStatement">=</span><span class="synIdentifier">{</span><span class="synStatement">(</span>e<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
e.preventDefault<span class="synStatement">();</span>
addUser<span class="synStatement">(</span><span class="synIdentifier">{</span>
variables: <span class="synIdentifier">{</span>
name: text<span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}}</span>
<span class="synStatement">></span>
<span class="synStatement"><</span>input
<span class="synStatement">type=</span><span class="synConstant">"text"</span>
value<span class="synStatement">=</span><span class="synIdentifier">{</span>text<span class="synIdentifier">}</span>
onChange<span class="synStatement">=</span><span class="synIdentifier">{</span><span class="synStatement">(</span>e<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
setText<span class="synStatement">(</span>e.currentTarget.value<span class="synStatement">);</span>
<span class="synIdentifier">}}</span>
/<span class="synStatement">></span>
<span class="synStatement"><</span>/form<span class="synStatement">></span>
<span class="synStatement"><</span>Status /<span class="synStatement">></span>
<span class="synStatement"><</span>/<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synStatement">function</span> App<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>ApolloProvider client<span class="synStatement">=</span><span class="synIdentifier">{</span>client<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>SearchUser /<span class="synStatement">></span>
<span class="synStatement"><</span>AddUser /<span class="synStatement">></span> <span class="synComment">// これを追加</span>
<span class="synStatement"><</span>/ApolloProvider<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<p>試しに<code>Alice</code>を追加しようとすると失敗するし(既に<code>Alice</code>が存在するため)、<code>Dave</code>を追加してから<code>Dave</code>を検索すると情報を取得できる。</p>
numb_86
Next.js で始める gRPC 通信
hatenablog://entry/13574176438062588233
2022-02-12T15:44:59+09:00
2022-02-12T15:44:59+09:00 サーバ・クライアント間の通信を gRPC で行う場合、インターフェイスを定義した共通のファイルから、サーバとクライアント双方のコードを生成することができる。 この記事では、インターフェイスの定義ファイルを作成するところから始めて、gRPC を利用した単純なウェブアプリを作っていく。 gRPC についての概念的な説明などは扱わず、実際に手元で動くウェブアプリを作ることで、gRPC を使った開発についてイメージしやすくなることを意図している。 Next.js では API Routes を使って API サーバを作ることができるが、それを gRPC クライアントとして実装する。 そのため、リクエス…
<p>サーバ・クライアント間の通信を gRPC で行う場合、インターフェイスを定義した共通のファイルから、サーバとクライアント双方のコードを生成することができる。<br />
この記事では、インターフェイスの定義ファイルを作成するところから始めて、gRPC を利用した単純なウェブアプリを作っていく。<br />
gRPC についての概念的な説明などは扱わず、実際に手元で動くウェブアプリを作ることで、gRPC を使った開発についてイメージしやすくなることを意図している。</p>
<p>Next.js では API Routes を使って API サーバを作ることができるが、それを gRPC クライアントとして実装する。<br />
そのため、リクエストの流れは以下のようになる。</p>
<pre class="code" data-lang="" data-unlink>Frontend == (REST) ==> API Routes == (gRPC) ==> gRPC Server</pre>
<p>動作確認は Node.js の<code>v16.13.2</code>で行っており、利用しているライブラリのバージョンは以下の通り。</p>
<ul>
<li>gRPC サーバ
<ul>
<li>@grpc/grpc-js@1.5.5</li>
<li>google-protobuf@3.19.4</li>
<li>grpc_tools_node_protoc_ts@5.3.2</li>
<li>grpc-tools@1.11.2</li>
<li>ts-node-dev@1.1.8</li>
<li>typescript@4.5.5</li>
</ul>
</li>
<li>gRPC クライアント
<ul>
<li>@grpc/grpc-js@1.5.5</li>
<li>@types/node@17.0.17</li>
<li>@types/react@17.0.39</li>
<li>eslint-config-next@12.0.10</li>
<li>eslint@8.9.0</li>
<li>google-protobuf@3.19.4</li>
<li>grpc_tools_node_protoc_ts@5.3.2</li>
<li>grpc-tools@1.11.2</li>
<li>next@12.0.10</li>
<li>react-dom@17.0.2</li>
<li>react@17.0.2</li>
<li>typescript@4.5.5</li>
</ul>
</li>
</ul>
<h2>proto ファイル</h2>
<p>まずは、「proto ファイル」と呼ばれる、インターフェイスを定義したファイルを作成する。</p>
<p>プロジェクトのルートディレクトリに<code>protos</code>ディレクトリを作り、そのなかに以下の内容の<code>user.proto</code>を作成する。</p>
<pre class="code lang-proto" data-lang="proto" data-unlink><span class="synPreProc">syntax</span> = <span class="synConstant">"proto3"</span>;
<span class="synStatement">service</span> UserManager {
<span class="synStatement">rpc</span> get (UserRequest) <span class="synStatement">returns</span> (UserResponse) {}
}
<span class="synType">message</span> User {
<span class="synType">uint32</span> id = <span class="synConstant">1</span>;
<span class="synType">string</span> name = <span class="synConstant">2</span>;
<span class="synType">bool</span> is_admin = <span class="synConstant">3</span>;
}
<span class="synType">message</span> UserRequest {
<span class="synType">uint32</span> id = <span class="synConstant">1</span>;
}
<span class="synType">message</span> UserResponse {
User user = <span class="synConstant">1</span>;
}
</pre>
<p><code>UserManager</code>というサービスを定義しており、このサービスは<code>get</code>という関数(プロシージャ)を持つ。<br />
<code>get</code>はパラメータとして<code>UserRequest</code>を受け取り、<code>UserResponse</code>を返す。</p>
<p>この proto ファイルからコードを生成して、クライアントやサーバの開発を行っていく。<br />
この記事ではどちらも TypeScript で開発するが、他の言語を使ってもよいし、クライアントとサーバで言語を揃える必要もない。<br />
<a href="https://grpc.io/docs/languages/">gRPC がサポートしている言語</a>なら、どの言語でも proto ファイルからコードを生成できる。</p>
<h2>gRPC サーバの開発</h2>
<p>プロジェクトのルートディレクトリに<code>server</code>ディレクトリを作り、そこで gRPC サーバの開発を行う。<br />
まずは開発に必要なライブラリをインストールする。</p>
<pre class="code" data-lang="" data-unlink>$ mkdir server
$ cd server
$ yarn add @grpc/grpc-js google-protobuf
$ yarn add -D grpc-tools grpc_tools_node_protoc_ts</pre>
<p>次に、<code>codegen</code>というディレクトリを作り、proto ファイルをコンパイルしてそこにコードを出力する。</p>
<pre class="code" data-lang="" data-unlink>$ mkdir codegen
$ yarn run grpc_tools_node_protoc --plugin=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:codegen --grpc_out=grpc_js:codegen --ts_out=grpc_js:codegen -I ../ ../protos/user.proto</pre>
<p>以下のように 4 つのファイルが生成されていれば成功。</p>
<pre class="code" data-lang="" data-unlink>$ ls -1 codegen/protos
user_grpc_pb.d.ts
user_grpc_pb.js
user_pb.d.ts
user_pb.js</pre>
<p>ここから実際にコードを書いていくので、TypeScript のセットアップを行う。</p>
<pre class="code" data-lang="" data-unlink>$ yarn add -D typescript
$ yarn run tsc --init</pre>
<p>次に、<code>src/index.ts</code>を作成し、以下のように書く。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span>
sendUnaryData<span class="synStatement">,</span>
Server<span class="synStatement">,</span>
ServerCredentials<span class="synStatement">,</span>
ServerUnaryCall<span class="synStatement">,</span>
<span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"@grpc/grpc-js"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> UserManagerService <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"../codegen/protos/user_grpc_pb"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> UserRequest<span class="synStatement">,</span> UserResponse<span class="synStatement">,</span> User <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"../codegen/protos/user_pb"</span><span class="synStatement">;</span>
<span class="synComment">// 実際には DB のような永続層から取得するはず</span>
<span class="synType">const</span> users <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Map</span><span class="synStatement">(</span><span class="synIdentifier">[</span>
<span class="synIdentifier">[</span><span class="synConstant">1</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">1</span><span class="synStatement">,</span> name: <span class="synConstant">"Alice"</span><span class="synStatement">,</span> isAdmin: <span class="synConstant">true</span> <span class="synIdentifier">}]</span><span class="synStatement">,</span>
<span class="synIdentifier">[</span><span class="synConstant">2</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">2</span><span class="synStatement">,</span> name: <span class="synConstant">"Bob"</span><span class="synStatement">,</span> isAdmin: <span class="synConstant">false</span> <span class="synIdentifier">}]</span><span class="synStatement">,</span>
<span class="synIdentifier">[</span><span class="synConstant">3</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">3</span><span class="synStatement">,</span> name: <span class="synConstant">"Carol"</span><span class="synStatement">,</span> isAdmin: <span class="synConstant">false</span> <span class="synIdentifier">}]</span><span class="synStatement">,</span>
<span class="synIdentifier">]</span><span class="synStatement">);</span>
<span class="synStatement">function</span> <span class="synStatement">get(</span>
call: ServerUnaryCall<span class="synStatement"><</span>UserRequest<span class="synStatement">,</span> UserResponse<span class="synStatement">>,</span>
callback: sendUnaryData<span class="synStatement"><</span>UserResponse<span class="synStatement">></span>
<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> requestId <span class="synStatement">=</span> call.request.getId<span class="synStatement">();</span>
<span class="synType">const</span> targetedUser <span class="synStatement">=</span> users.<span class="synStatement">get(</span>requestId<span class="synStatement">);</span>
<span class="synType">const</span> response <span class="synStatement">=</span> <span class="synStatement">new</span> UserResponse<span class="synStatement">();</span>
<span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>targetedUser<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synSpecial">throw</span> <span class="synStatement">new</span> <span class="synSpecial">Error</span><span class="synStatement">(</span><span class="synConstant">"User is not found."</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">new</span> User<span class="synStatement">();</span>
user.setId<span class="synStatement">(</span>targetedUser.id<span class="synStatement">);</span>
user.setName<span class="synStatement">(</span>targetedUser.name<span class="synStatement">);</span>
user.setIsAdmin<span class="synStatement">(</span>targetedUser.isAdmin<span class="synStatement">);</span>
response.setUser<span class="synStatement">(</span>user<span class="synStatement">);</span>
callback<span class="synStatement">(</span><span class="synType">null</span><span class="synStatement">,</span> response<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">function</span> startServer<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> server <span class="synStatement">=</span> <span class="synStatement">new</span> Server<span class="synStatement">();</span>
server.addService<span class="synStatement">(</span>UserManagerService<span class="synStatement">,</span> <span class="synIdentifier">{</span> <span class="synStatement">get</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
server.bindAsync<span class="synStatement">(</span>
<span class="synConstant">"0.0.0.0:50051"</span><span class="synStatement">,</span>
ServerCredentials.createInsecure<span class="synStatement">(),</span>
<span class="synStatement">(</span>error<span class="synStatement">,</span> port<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>error<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synSpecial">console</span>.error<span class="synStatement">(</span>error<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
server.start<span class="synStatement">();</span>
<span class="synSpecial">console</span>.log<span class="synStatement">(</span><span class="synConstant">`server start listing on port </span><span class="synSpecial">${</span>port<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
startServer<span class="synStatement">();</span>
</pre>
<p>先程生成した<code>user_grpc_pb</code>や<code>user_pb</code>を import し、それを使ってコードを書いている。</p>
<p>最後に、<code>ts-node-dev</code>を使ってサーバを起動する。</p>
<pre class="code" data-lang="" data-unlink>$ yarn add -D ts-node-dev
$ yarn run ts-node-dev src/index.ts</pre>
<p><code>server start listing on port 50051</code>と表示されれば成功。</p>
<h2>gRPC クライアントの開発</h2>
<p>クライアント側は Next.js を使うため、プロジェクトのルートディレクトリに戻って以下のコマンドを実行する。</p>
<pre class="code" data-lang="" data-unlink>$ yarn create next-app client --ts</pre>
<p>この時点で、以下のようなディレクトリ構成になっているはず。</p>
<pre class="code" data-lang="" data-unlink>$ tree ./ -L 2
./
├── client
│ ├── README.md
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── node_modules
│ ├── package.json
│ ├── pages
│ ├── public
│ ├── styles
│ ├── tsconfig.json
│ └── yarn.lock
├── protos
│ └── user.proto
└── server
├── codegen
├── node_modules
├── package.json
├── src
├── tsconfig.json
└── yarn.lock</pre>
<p>以降は、<code>client</code>に移動して Next.js での開発を行う。</p>
<p>まずはサーバのときと同様、gRPC 関連のライブラリのインストールと、proto ファイルのコンパイルを行う。</p>
<pre class="code" data-lang="" data-unlink>$ yarn add @grpc/grpc-js google-protobuf
$ yarn add -D grpc-tools grpc_tools_node_protoc_ts
$ mkdir codegen
$ yarn run grpc_tools_node_protoc --plugin=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:codegen --grpc_out=grpc_js:codegen --ts_out=grpc_js:codegen -I ../ ../protos/user.proto</pre>
<p>続いて、以下の内容の<code>pages/api/user.ts</code>を作る。これが gRPC クライアントとして機能する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synStatement">type</span> <span class="synIdentifier">{</span> NextApiRequest<span class="synStatement">,</span> NextApiResponse <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"next"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> credentials<span class="synStatement">,</span> ServiceError <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"@grpc/grpc-js"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> UserManagerClient <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"../../codegen/protos/user_grpc_pb"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> UserRequest<span class="synStatement">,</span> UserResponse <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"../../codegen/protos/user_pb"</span><span class="synStatement">;</span>
<span class="synType">const</span> Request <span class="synStatement">=</span> <span class="synStatement">new</span> UserRequest<span class="synStatement">();</span>
<span class="synType">const</span> Client <span class="synStatement">=</span> <span class="synStatement">new</span> UserManagerClient<span class="synStatement">(</span>
<span class="synConstant">"localhost:50051"</span><span class="synStatement">,</span>
credentials.createInsecure<span class="synStatement">()</span>
<span class="synStatement">);</span>
<span class="synStatement">export</span> <span class="synStatement">type</span> UserApiResponse <span class="synStatement">=</span>
| <span class="synIdentifier">{</span> ok: <span class="synConstant">true</span><span class="synStatement">;</span> user: UserResponse.AsObject<span class="synIdentifier">[</span><span class="synConstant">"user"</span><span class="synIdentifier">]</span> <span class="synIdentifier">}</span>
| <span class="synIdentifier">{</span> ok: <span class="synConstant">false</span><span class="synStatement">;</span> error: ServiceError <span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synStatement">function</span> handler<span class="synStatement">(</span>
apiReq: NextApiRequest<span class="synStatement">,</span>
apiRes: NextApiResponse<span class="synStatement"><</span>UserApiResponse<span class="synStatement">></span>
<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> id <span class="synIdentifier">}</span> <span class="synStatement">=</span> <span class="synSpecial">JSON</span>.parse<span class="synStatement">(</span>apiReq.body<span class="synStatement">);</span>
Request.setId<span class="synStatement">(</span>id<span class="synStatement">);</span>
Client.<span class="synStatement">get(</span>Request<span class="synStatement">,</span> <span class="synStatement">(</span>grpcErr<span class="synStatement">,</span> grpcRes<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>grpcErr<span class="synStatement">)</span> <span class="synIdentifier">{</span>
apiRes.<span class="synStatement">status(</span><span class="synConstant">500</span><span class="synStatement">)</span>.json<span class="synStatement">(</span><span class="synIdentifier">{</span> ok: <span class="synConstant">false</span><span class="synStatement">,</span> error: grpcErr <span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span> <span class="synStatement">else</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> user <span class="synIdentifier">}</span> <span class="synStatement">=</span> grpcRes.toObject<span class="synStatement">();</span>
apiRes.<span class="synStatement">status(</span><span class="synConstant">200</span><span class="synStatement">)</span>.json<span class="synStatement">(</span><span class="synIdentifier">{</span> ok: <span class="synConstant">true</span><span class="synStatement">,</span> user <span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<p>gRPC サーバと同様、proto ファイルから生成されたコードを使って実装している。</p>
<p>最後に、<code>pages/index.tsx</code>を編集して UI を作る。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synStatement">type</span> <span class="synIdentifier">{</span> NextPage <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"next"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> useState<span class="synStatement">,</span> Fragment<span class="synStatement">,</span> ChangeEvent <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"react"</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synStatement">type</span> <span class="synIdentifier">{</span> UserApiResponse <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"./api/user"</span><span class="synStatement">;</span>
<span class="synType">const</span> App: NextPage <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>result<span class="synStatement">,</span> setResult<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement"><</span><span class="synType">string</span><span class="synStatement">>(</span><span class="synConstant">""</span><span class="synStatement">);</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>selectedId<span class="synStatement">,</span> setSelectedId<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement"><</span><span class="synType">number</span><span class="synStatement">>();</span>
<span class="synType">const</span> handleChange <span class="synStatement">=</span> <span class="synStatement">async</span> <span class="synStatement">(</span>e: ChangeEvent<span class="synStatement"><</span>HTMLInputElement<span class="synStatement">>)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> id <span class="synStatement">=</span> <span class="synSpecial">Number</span><span class="synStatement">(</span>e.currentTarget.value<span class="synStatement">);</span>
setSelectedId<span class="synStatement">(</span>id<span class="synStatement">);</span>
<span class="synType">const</span> res <span class="synStatement">=</span> <span class="synStatement">await</span> <span class="synSpecial">fetch</span><span class="synStatement">(</span><span class="synConstant">"/api/user"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
method: <span class="synConstant">"POST"</span><span class="synStatement">,</span>
body: <span class="synSpecial">JSON</span>.stringify<span class="synStatement">(</span><span class="synIdentifier">{</span> id <span class="synIdentifier">}</span><span class="synStatement">),</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synType">const</span> json: UserApiResponse <span class="synStatement">=</span> <span class="synStatement">await</span> res.json<span class="synStatement">();</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>json.ok<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> user <span class="synIdentifier">}</span> <span class="synStatement">=</span> json<span class="synStatement">;</span>
setResult<span class="synStatement">(</span><span class="synSpecial">JSON</span>.stringify<span class="synStatement">(</span>user<span class="synStatement">));</span>
<span class="synIdentifier">}</span> <span class="synStatement">else</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> code<span class="synStatement">,</span> details <span class="synIdentifier">}</span> <span class="synStatement">=</span> json.error<span class="synStatement">;</span>
setResult<span class="synStatement">(</span><span class="synConstant">`Error! </span><span class="synSpecial">${</span>code<span class="synSpecial">}</span><span class="synConstant">: </span><span class="synSpecial">${</span>details<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>div<span class="synStatement">></span>
<span class="synIdentifier">{[</span>...<span class="synSpecial">Array</span><span class="synStatement">(</span><span class="synConstant">3</span><span class="synStatement">)</span><span class="synIdentifier">]</span>.map<span class="synStatement">((</span>_<span class="synStatement">,</span> index<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> id <span class="synStatement">=</span> index + <span class="synConstant">1</span><span class="synStatement">;</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>Fragment key<span class="synStatement">=</span><span class="synIdentifier">{</span>id<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>input
<span class="synStatement">type=</span><span class="synConstant">"radio"</span>
value<span class="synStatement">=</span><span class="synIdentifier">{</span>id<span class="synIdentifier">}</span>
onChange<span class="synStatement">=</span><span class="synIdentifier">{</span>handleChange<span class="synIdentifier">}</span>
checked<span class="synStatement">=</span><span class="synIdentifier">{</span>id <span class="synStatement">===</span> selectedId<span class="synIdentifier">}</span>
/<span class="synStatement">></span>
<span class="synIdentifier">{</span>id<span class="synIdentifier">}{</span><span class="synConstant">" "</span><span class="synIdentifier">}</span>
<span class="synStatement"><</span>/Fragment<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span>
<span class="synStatement"><</span>p<span class="synStatement">></span><span class="synIdentifier">{</span>result<span class="synIdentifier">}</span><span class="synStatement"><</span>/p<span class="synStatement">></span>
<span class="synStatement"><</span>/div<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> App<span class="synStatement">;</span>
</pre>
<p>gRPC サーバが起動している状態で<code>$ yarn run dev</code>して<code>http://localhost:3000/</code>にアクセスすると、選択したチェックボックスに応じて表示が変わる。<br />
例えば<code>1</code>を選択すると以下が表示されるはず。</p>
<pre class="code" data-lang="" data-unlink>{"id":1,"name":"Alice","isAdmin":true}</pre>
<p>gRPC サーバが起動していない場合はエラーメッセージが表示される。</p>
<pre class="code" data-lang="" data-unlink>Error! 14: No connection established</pre>
numb_86
引きこもり・日記・エンジニア人生
hatenablog://entry/13574176438057916449
2022-01-29T20:19:35+09:00
2022-11-23T15:03:39+09:00 2 年ぶりに労働し始めたことでブログの更新頻度が露骨に落ちているが、文章を全く書いていないわけではなく、折に触れて社内で長文を投下している。 社内向けの怪文書ばかり書いていて、パブリックなブログを全然書けない。— なむ (@numb_86) 2021年12月29日 内容は本当に個人的なものというか、自分が考えていることや思っていることを書いているだけで、ブログと同じノリでやっている。 これは私だけがやっているわけではなく、例えば代表の庄田も最近考えていることや意識していることを scrapbox に書いていて、いくつかはブログの一部として社外にも公開されている。 自分の考えを文章にすることは私…
<p>2 年ぶりに労働し始めたことでブログの更新頻度が露骨に落ちているが、文章を全く書いていないわけではなく、折に触れて社内で長文を投下している。</p>
<p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">社内向けの怪文書ばかり書いていて、パブリックなブログを全然書けない。</p>— なむ (@numb_86) <a href="https://twitter.com/numb_86/status/1476209192155619328?ref_src=twsrc%5Etfw">2021年12月29日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p>
<p>内容は本当に個人的なものというか、自分が考えていることや思っていることを書いているだけで、ブログと同じノリでやっている。<br/>
これは私だけがやっているわけではなく、例えば代表の庄田も最近考えていることや意識していることを scrapbox に書いていて、いくつかはブログの一部として社外にも公開されている。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ffabichirox" title="Ichiro Shoda @HERP ,inc.|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p>自分の考えを文章にすることは私にとって自然なことなので深く考えずに書いていたのだが、その結果、会社から特別手当が支給された。</p>
<p>勤務先の <a href="https://herp.co.jp/">HERP</a> ではクオーター単位で全社的な振り返りを行っているのだが、そのタイミングで、会社が掲げているバリューを体現している人を対象に表彰が行われる(今日現在)。<br/>
私はバリューのひとつである「ストレートに話す」(現在は「ストレートに伝える」)で表彰され、お金をもらえた。<br/>
もともと文章を書くのは好きだったのが、それがこうやって何かの役に立つこともある、あるいは有利に働くこともあるんだなと思った。</p>
<h2 id="引きこもりと日記">引きこもりと日記</h2>
<p>私が文章を書くようになったキッカケは明確で、引きこもり始めた前後に書くようになった。それまではそういう習慣はなかった。<br/>
自分が感じているつらさを他者に伝えることができない、いつの間にか話をはぐらかされている、そういうのがすごく悔しくて、論理的に話せるようになろうと思って、書き始めた。<br/>
やがて、他者に伝えるためではなく、自分の考えを整理するために書くようになった。<br/>
自分は何が苦しいのか、なぜ上手くやれないのか、どうすれば状況を打開できるのか。それを整理するために、思考と言語化を行ったり来たりしていた。文章にしないと思考がまとまらないし、後で振り返ることもできない。だから、考え事をするときは日記という形で文章に残すようになった。</p>
<p>日記を書くのが習慣になることで、文章を書くことが苦ではなくなった。<br/>
かなりエネルギーを使うので、余力がない、時間を捻出できない、ということはあるが、文章を書くこと自体はとても楽しい。<br/>
このブログも、書くこと自体がストレス解消になっている。書いている最中も楽しいし、書き上げることができれば、その時点で達成感や満足感がある。<br/>
だから続けてこれた。アクセス数や反響を重視していたら、ここまで続けてこれなかったと思う。あくまでも自分のために書いているからこそ、続けてこれた。</p>
<p>引きこもりになってよかったと思ったことなんて一度もないし、恐らくこれからもない。<br/>
機会損失はあまりにも大きかったし、スキルやスペックに相当な偏りのある人間になってしまったと思う。<br/>
しかし、「失ったものが大きすぎる」からといって、「得たものが何もない」というわけではない。<br/>
考えを文章として表現する習慣や書きながら思考を整理する習慣というのは、引きこもっていなかったら身につかなかった。</p>
<h2 id="ブログとキャリア">ブログとキャリア</h2>
<p>そしてその習慣に助けられたというか、その習慣のおかげで今もソフトウェアエンジニアとして食べていけている気がする。</p>
<p>自分のために書いているこのブログだが、キャリアの武器としてもそれなりに機能していると思っている。<br/>
「30 代」「エンジニアとしての実務経験ゼロ」「エンジニアの知り合いもいない」という私がなんとか業界に潜り込むためには、アウトプットは絶対に必要だと思っていた。そのうちのひとつが、このブログだった。独学を始めて 1 年弱くらいで就職活動を行ったが、「初心者にしては」というエクスキューズはつくものの、ブログによる発信は概ね好評だったと思う。そして無事、希望した企業に入ることができた。</p>
<p>優秀なフロントエンドエンジニアが在籍している会社で、その方がいるから入社したのだが、その方にプログラミングの初歩を教えてもらったおかげで今の自分がいる。<br/>
以下の記事の内容の大部分はその方に教えてもらったことであり、私にとっての師匠である。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2017%2F08%2F26%2F095327" title="入社からの半年間でコードレビューで指摘されたことのまとめ - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p>ブログというアウトプットがなければ、この会社に入れなかったかもしれない(本当に実務経験ゼロだったからその可能性は十分ある)。そう考えると、ブログを書いていたおかげで今のキャリアがあると言える。<br/>
これは全くの余談だが、師匠と連絡を取る機会が最近あり、このブログによる発信を褒めて頂いて嬉しかった。</p>
<p>そしてブログによる発信を続けてきたおかげで、社外のエンジニアの方と知り合い、交流する機会も増えてきた。私よりもはるかに優秀なエンジニアの方が私の存在を知ってくれていたりする。</p>
<p>プログラミングを始めたのが 30 歳からで、かつ才覚に乏しい(社内のエンジニアを見てると本当にそう思う。なんで Haskell や Rust をスラスラ書けるのか分からないし、なんで Nix をいきなり使えたりするのかも分からない。)自分がなんとかキャリアを作れているのは、引きこもり時代に培われた「文章を書くのが苦にならない」という特性に支えられている部分が意外と大きいのかもしれない。</p>
<h2 id="特性と環境">特性と環境</h2>
<p>特性それ自体に良し悪しはないと思っていて、それが活きるも死ぬも環境次第だと思っている。<br/>
「文章を書くのが苦にならない」という特性はエンジニアと相性がよかった。この界隈は個人がブログで発信するのは珍しくなく、基本的には歓迎される。インターネットとの相性のよさ、標準化された知識、など色々と理由はあると思うが、こういう業界は珍しい。</p>
<p>冒頭で述べた表彰にも同じことが言えて、私が「ストレートに伝える」ことができたのは、環境のおかげに過ぎない。<br/>
私とある程度以上の付き合いがある人なら分かると思うが、私は決して率直に発言していくようなタイプではない。むしろ「様子見」や「日和見」に徹するタイプであり、新しく入ったばかりの組織なら、なおさらそうする。<br/>
そういう私が問題意識を率直に伝えたり、「よくない」と思ったことについて「言ったほうがいい」「言おう」と思えるのは、かなりすごいことだと思う。</p>
<p>これは、オープンな社風によるところが大きい。<br/>
代表の庄田が以下のような記事を書いているが、実際に、かなり高い度合いで「オープンな組織」を実現できていると思う。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ffabichirox%2Fn%2Fn22a50a862f6b" title="【週刊私の気持ち No.1】オープンであることはなぜ重要か|Ichiro Shoda @HERP ,inc.|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p>もうすぐ創業 5 年を迎え、正社員だけでも 50 人弱いる組織でこの透明性を維持できているのは、かなりすごいと思う。<br/>
これまで「伝統的な日本企業」や「Slack のコミュニケーションの大部分がプライベートチャンネル」という会社で働いてきたこともあり、なおさらそう思う。</p>
<p>引きこもりに苦しみながら日記を書いていた経験がこういう形でつながってくることがあるとは勿論思っておらず、何となく感慨深かったので、書いてみた。</p>
numb_86
React の新しい概念「トランジション」で React アプリの応答性を改善する
hatenablog://entry/13574176438048766031
2022-01-02T22:54:02+09:00
2022-11-23T15:03:18+09:00 React v18 には多くの改善や新機能が盛り込まれる予定だが、そのなかでも特に注目を集めると思われるのが、Concurrent Features と呼ばれる一連の機能。 これらの機能を使うことで、コンポーネントのレンダリングについてより柔軟な設定が可能になり、上手く使えばパフォーマンスや UX の向上を実現できる。 この記事では Concurrent Features のひとつであるstartTransitionと、それを使いこなす上で重要な概念である「トランジション」について説明する。 この記事ではコンセプトの説明や具体例の提示のみを行う。詳細を知りたい場合は以下を参照。 一年前の記事で…
<p>React v18 には多くの改善や新機能が盛り込まれる予定だが、そのなかでも特に注目を集めると思われるのが、Concurrent Features と呼ばれる一連の機能。<br/>
これらの機能を使うことで、コンポーネントのレンダリングについてより柔軟な設定が可能になり、上手く使えばパフォーマンスや UX の向上を実現できる。<br/>
この記事では Concurrent Features のひとつである<code>startTransition</code>と、それを使いこなす上で重要な概念である「トランジション」について説明する。</p>
<p>この記事ではコンセプトの説明や具体例の提示のみを行う。詳細を知りたい場合は以下を参照。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnumb86-tech.hatenablog.com%2Fentry%2F2020%2F12%2F16%2F214705" title="React の Concurrent Mode を使ってみる(2020年12月版) - 30歳からのプログラミング" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p>一年前の記事であるため古くなっている部分もあるが、根幹は大きく変わっていないと認識している。<br/>
なお、上記の記事には「Concurrent Mode」という用語がタイトルに入っているが、これは今後は使われなくなっていくと思われる。<br/>
v18 へのアップグレードをよりスムーズに行えるようにするため、アプローチを変えたとのこと。</p>
<blockquote><p>我々は段階的な導入に向けてのアップグレード戦略を再設計しました。イチかゼロかの「モード」の代わりに、並行レンダリングは新機能のどれかを利用するような更新がある場合にのみ有効化されるようになりました。実用上、これはつまり書き換えをせずに React 18 を導入し、自分のペースで React 18 の新機能を試していけるようになるということです。</p></blockquote>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fja.reactjs.org%2Fblog%2F2021%2F06%2F08%2Fthe-plan-for-react-18.html" title="React 18に向けてのプラン – React Blog" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe>
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Freactwg%2Freact-18%2Fdiscussions%2F64" title="What happened to concurrent "mode"? · Discussion #64 · reactwg/react-18" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p>動作確認に使用したライブラリのバージョンは以下の通り。</p>
<ul>
<li>next@12.0.7</li>
<li>react@18.0.0-beta-24dd07bd2-20211208</li>
<li>react-dom@18.0.0-beta-24dd07bd2-20211208</li>
<li>typescript@4.5.4</li>
<li>@types/react@17.0.38</li>
<li>@types/react-dom@17.0.11</li>
</ul>
<p>React v18 はまだ正式リリースされていないので、ベータ版を使う。</p>
<h2 id="トランジションとは何か">トランジションとは何か</h2>
<p>React は原則的に、データと UI が一対一になっている。データと UI はシンクロしており、データが変化すればそれに対応した UI が新しく作られる。<br/>
「状態」もデータの一部なので、「状態変化」によっても、UI の構築は行われる。</p>
<p>以下の例では、ボタンを押す毎に<code>state</code>がインクリメントされるため、その度に UI が作られ、表示されている数字も増えていく。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> useState <span class="synIdentifier">}</span> from <span class="synConstant">"react"</span>;
<span class="synIdentifier">function</span> App() <span class="synIdentifier">{</span>
<span class="synStatement">const</span> <span class="synIdentifier">[</span>state, setState<span class="synIdentifier">]</span> = useState(1);
<span class="synStatement">return</span> (
<>
<button
onClick=<span class="synIdentifier">{</span>() => <span class="synIdentifier">{</span>
setState((s) => s + 1);
<span class="synIdentifier">}}</span>
>
count up
</button><span class="synIdentifier">{</span><span class="synConstant">" "</span><span class="synIdentifier">}</span>
<span><span class="synIdentifier">{</span>state<span class="synIdentifier">}</span></span>
</>
);
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> App;
</pre>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20220102/20220102221632.gif" width="200" height="62" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>状態が更新される度にレンダリングが行われ、最新の状態と対になる UI が作られる。これが React の原則である。</p>
<p>上記の例では状態はひとつだけ(<code>state</code>)だったが、現実の React アプリでは複数存在することが多い。<br/>
そして、ひとつのイベントによって複数の状態が更新されることも珍しくない。その場合 React は、ひとつずつ UI に反映させるのではなく、全ての状態更新が反映された UI を作る。</p>
<p>例えば、何らかのイベントによって<code>A</code>と<code>B</code>という二つの状態が更新された場合、まず最新の<code>A</code>を反映した UI を作り、続いて最新の<code>B</code>も反映した UI を作る、のではなく、最新の<code>A</code>と<code>B</code>が反映された UI を一度に作る。</p>
<p>これは<code>A</code>と<code>B</code>を同列に扱うということだが、言い換えれば、これまでの React では、レンダリングに対して優先度をつけることが出来なかったのである。<br/>
<code>A</code>についてはすぐに UI に反映させたいからまずは<code>A</code>を優先的にレンダリングする、ということができない。このため、何らかの理由で<code>B</code>のレンダリングに時間がかかってしまう場合、それが終わるまで<code>A</code>の更新も UI には反映されなくなってしまう。例え<code>A</code>の反映そのものはすぐに終わるものだったとしても。</p>
<p>トランジションはこの問題を解決する。<br/>
トランジションとは「優先度が低い状態更新」のことであり、どの状態更新がトランジションであるかを指定する形で、レンダリングに優先順位をつけることができる。<br/>
React アプリの開発者は、<code>startTransition</code>を使うことで、「この状態更新はトランジションです」と React に指示を出せるようになるのである。<br/>
これを上手く使うことで、アプリの操作性を大幅に改善できる可能性がある。</p>
<p>ここからは、具体的な例を示しながら<code>startTransition</code>の使い方を説明していく。</p>
<h2 id="環境のセットアップ">環境のセットアップ</h2>
<p>サンプルアプリを作るための環境構築を行っていく。簡便なので今回は Next.js を使っているが、<code>startTransition</code>を使う上で Next.js は必須ではない。</p>
<p>まずは Next.js の環境を構築する。</p>
<pre class="code" data-lang="" data-unlink>$ yarn create next-app --ts</pre>
<p>次に、React のベータ版をインストールする。TypeScript で開発するので型もインストールする。</p>
<pre class="code" data-lang="" data-unlink>$ yarn add react@beta react-dom@beta
$ yarn add -D @types/react @types/react-dom</pre>
<p>最後に、<code>tsconfig.json</code>を編集する。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink> "resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
<span class="synSpecial">- "incremental": true</span>
<span class="synIdentifier">+ "incremental": true,</span>
<span class="synIdentifier">+ "types": ["react/next", "react-dom/next"]</span>
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
</pre>
<p>Next.js の v12 ではこれだけで<code>startTransition</code>を使えるようになる。</p>
<h2 id="サンプルアプリの作成">サンプルアプリの作成</h2>
<p>サンプルアプリとして、メンバー毎のタスクを表示するアプリを開発する。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20220102/20220102221619.png" width="374" height="492" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>まずはメンバーとタスクの型を定義する。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> Member <span class="synStatement">=</span> <span class="synConstant">"Alice"</span> | <span class="synConstant">"Bob"</span> | <span class="synConstant">"Carol"</span><span class="synStatement">;</span>
<span class="synStatement">type</span> Task <span class="synStatement">=</span> <span class="synIdentifier">{</span>
id: <span class="synType">number</span><span class="synStatement">;</span>
assignee: Member<span class="synStatement">;</span>
title: <span class="synType">string</span><span class="synStatement">;</span>
description: <span class="synType">string</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<p>続いてタスクリストを作成。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> allTasks: Task<span class="synIdentifier">[]</span> <span class="synStatement">=</span> <span class="synIdentifier">[</span>
<span class="synIdentifier">{</span>
id: <span class="synConstant">1</span><span class="synStatement">,</span>
assignee: <span class="synConstant">"Alice"</span><span class="synStatement">,</span>
title: <span class="synConstant">"React 学習"</span><span class="synStatement">,</span>
description: <span class="synConstant">"v18 の機能についてキャッチアップする"</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synComment">// 同じ要領で適当にタスクを作っていく</span>
<span class="synIdentifier">]</span>
</pre>
<p>あとは、これらを元にして UI を作っていく。<br/>
重要な箇所は適宜説明していくので、一旦読み飛ばしても構わない。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> TaskCard<span class="synStatement">(</span>task: Task<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> assignee<span class="synStatement">,</span> title<span class="synStatement">,</span> description <span class="synIdentifier">}</span> <span class="synStatement">=</span> task<span class="synStatement">;</span>
<span class="synType">const</span> getBorderColor <span class="synStatement">=</span> <span class="synStatement">(</span>member: Member<span class="synStatement">)</span>: <span class="synType">string</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>member <span class="synStatement">===</span> <span class="synConstant">"Alice"</span><span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synConstant">"aqua"</span><span class="synStatement">;</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>member <span class="synStatement">===</span> <span class="synConstant">"Bob"</span><span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synConstant">"lime"</span><span class="synStatement">;</span>
<span class="synStatement">return</span> <span class="synConstant">"orange"</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>div
style<span class="synStatement">=</span><span class="synIdentifier">{{</span>
border: <span class="synConstant">`5px solid </span><span class="synSpecial">${</span>getBorderColor(assignee)<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">,</span>
borderRadius: <span class="synConstant">"18px"</span><span class="synStatement">,</span>
margin: <span class="synConstant">"20px"</span><span class="synStatement">,</span>
padding: <span class="synConstant">"10px"</span><span class="synStatement">,</span>
width: <span class="synConstant">"200px"</span><span class="synStatement">,</span>
<span class="synIdentifier">}}</span>
<span class="synStatement">></span>
<span class="synStatement"><</span>div style<span class="synStatement">=</span><span class="synIdentifier">{{</span> fontSize: <span class="synConstant">"22px"</span> <span class="synIdentifier">}}</span><span class="synStatement">></span><span class="synIdentifier">{</span>title<span class="synIdentifier">}</span><span class="synStatement"><</span>/div<span class="synStatement">></span>
<span class="synStatement"><</span>div style<span class="synStatement">=</span><span class="synIdentifier">{{</span> fontSize: <span class="synConstant">"14px"</span><span class="synStatement">,</span> fontWeight: <span class="synConstant">"bold"</span> <span class="synIdentifier">}}</span><span class="synStatement">></span><span class="synIdentifier">{</span>assignee<span class="synIdentifier">}</span><span class="synStatement"><</span>/div<span class="synStatement">></span>
<span class="synStatement"><</span>hr /<span class="synStatement">></span>
<span class="synStatement"><</span>div style<span class="synStatement">=</span><span class="synIdentifier">{{</span> fontSize: <span class="synConstant">"14px"</span> <span class="synIdentifier">}}</span><span class="synStatement">></span><span class="synIdentifier">{</span>description<span class="synIdentifier">}</span><span class="synStatement"><</span>/div<span class="synStatement">></span>
<span class="synStatement"><</span>/div<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">function</span> TaskCardList<span class="synStatement">(</span><span class="synIdentifier">{</span> taskList <span class="synIdentifier">}</span>: <span class="synIdentifier">{</span> taskList: Task<span class="synIdentifier">[]</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> taskList.length <span class="synStatement">></span> <span class="synConstant">0</span> ? <span class="synStatement">(</span>
<span class="synStatement"><</span>ul style<span class="synStatement">=</span><span class="synIdentifier">{{</span> listStyleType: <span class="synConstant">"none"</span> <span class="synIdentifier">}}</span><span class="synStatement">></span>
<span class="synIdentifier">{</span>taskList.map<span class="synStatement">((</span>task<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>li key<span class="synStatement">=</span><span class="synIdentifier">{</span>task.id<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>TaskCard <span class="synIdentifier">{</span>...task<span class="synIdentifier">}</span> /<span class="synStatement">></span>
<span class="synStatement"><</span>/li<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span>
<span class="synStatement"><</span>/ul<span class="synStatement">></span>
<span class="synStatement">)</span> : <span class="synType">null</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span>
<span class="synStatement">function</span> App<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>selectedMember<span class="synStatement">,</span> setSelectedMember<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement"><</span>Member<span class="synStatement">>();</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>headline<span class="synStatement">,</span> setHeadline<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement"><</span><span class="synType">string</span><span class="synStatement">>(</span><span class="synConstant">""</span><span class="synStatement">);</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>taskList<span class="synStatement">,</span> setTaskList<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement"><</span>Task<span class="synIdentifier">[]</span><span class="synStatement">>(</span><span class="synIdentifier">[]</span><span class="synStatement">);</span>
<span class="synType">const</span> members: Member<span class="synIdentifier">[]</span> <span class="synStatement">=</span> <span class="synIdentifier">[</span><span class="synConstant">"Alice"</span><span class="synStatement">,</span> <span class="synConstant">"Bob"</span><span class="synStatement">,</span> <span class="synConstant">"Carol"</span><span class="synIdentifier">]</span><span class="synStatement">;</span>
<span class="synType">const</span> handleChange <span class="synStatement">=</span> <span class="synStatement">(</span>e: ChangeEvent<span class="synStatement"><</span>HTMLInputElement<span class="synStatement">>)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> member <span class="synStatement">=</span> e.currentTarget.value <span class="synStatement">as</span> Member<span class="synStatement">;</span>
setSelectedMember<span class="synStatement">(</span>member<span class="synStatement">);</span>
setHeadline<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">${</span>member<span class="synSpecial">}</span><span class="synConstant">'s task`</span><span class="synStatement">);</span>
setTaskList<span class="synStatement">(()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> allTasks.filter<span class="synStatement">((</span>t<span class="synStatement">)</span> <span class="synStatement">=></span> t.assignee <span class="synStatement">===</span> member<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><></span>
<span class="synIdentifier">{</span>members.map<span class="synStatement">((</span>m<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>Fragment key<span class="synStatement">=</span><span class="synIdentifier">{</span>m<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>input
<span class="synStatement">type=</span><span class="synConstant">"radio"</span>
value<span class="synStatement">=</span><span class="synIdentifier">{</span>m<span class="synIdentifier">}</span>
onChange<span class="synStatement">=</span><span class="synIdentifier">{</span>handleChange<span class="synIdentifier">}</span>
checked<span class="synStatement">=</span><span class="synIdentifier">{</span>m <span class="synStatement">===</span> selectedMember<span class="synIdentifier">}</span>
/<span class="synStatement">></span>
<span class="synIdentifier">{</span>m<span class="synIdentifier">}{</span><span class="synConstant">" "</span><span class="synIdentifier">}</span>
<span class="synStatement"><</span>/Fragment<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span>
<span class="synStatement"><</span>h1<span class="synStatement">></span><span class="synIdentifier">{</span>headline<span class="synIdentifier">}</span><span class="synStatement"><</span>/h1<span class="synStatement">></span>
<span class="synStatement"><</span>TaskCardList taskList<span class="synStatement">=</span><span class="synIdentifier">{</span>taskList<span class="synIdentifier">}</span> /<span class="synStatement">></span>
<span class="synStatement"><</span>/<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> App<span class="synStatement">;</span>
</pre>
<p>問題なく動いている。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20220102/20220102221233.gif" width="412" height="500" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="応答性の悪さは-UX-を大きく損なう">応答性の悪さは UX を大きく損なう</h2>
<p>快適に動いているこのアプリだったが、何らかの理由でタスク一覧の表示に時間がかかるようになってしまったとする。<br/>
アプリが複雑になって処理が重くなったのかもしれないし、通信している API サーバが遅くなってしまったのかもしれない。ユーザーが使っている端末の性能が高くない場合にも、同様の問題は起こり得る。</p>
<p>その状況を擬似的に再現するため、自作の<code>sleep</code>関数で処理を遅延させてみる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> sleep<span class="synStatement">(</span>ms: <span class="synType">number</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> startTime <span class="synStatement">=</span> performance.now<span class="synStatement">();</span>
<span class="synStatement">while</span> <span class="synStatement">(</span>performance.now<span class="synStatement">()</span> - startTime <span class="synStatement"><</span> ms<span class="synStatement">);</span>
<span class="synIdentifier">}</span>
</pre>
<pre class="code lang-typescript" data-lang="typescript" data-unlink> <span class="synType">const</span> handleChange <span class="synStatement">=</span> <span class="synStatement">(</span>e: ChangeEvent<span class="synStatement"><</span>HTMLInputElement<span class="synStatement">>)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> member <span class="synStatement">=</span> e.currentTarget.value <span class="synStatement">as</span> Member<span class="synStatement">;</span>
setSelectedMember<span class="synStatement">(</span>member<span class="synStatement">);</span>
setHeadline<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">${</span>member<span class="synSpecial">}</span><span class="synConstant">'s task`</span><span class="synStatement">);</span>
setTaskList<span class="synStatement">(()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
sleep<span class="synStatement">(</span><span class="synConstant">1500</span><span class="synStatement">);</span> <span class="synComment">// これを追加</span>
<span class="synStatement">return</span> allTasks.filter<span class="synStatement">((</span>t<span class="synStatement">)</span> <span class="synStatement">=></span> t.assignee <span class="synStatement">===</span> member<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<p>このようにして改めて触ってみると、操作性が大幅に悪化していることが分かる。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20220102/20220102221114.gif" width="412" height="500" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>表示速度が遅いこと自体が望ましくないのだが、より大きな問題は、ユーザーがラジオボタンをクリックしたときにアプリが何も反応しないことである。<br/>
ユーザーに対するフィードバックが何もないため、自分が操作を間違ったのか、アプリが固まってしまったのか、単に処理に時間が掛かっているだけなのか、ユーザーからは何も分からない。これは大きなストレスになるし、お世辞にも使いやすいアプリとは言い難い。<br/>
ウェブアプリのパフォーマンスを考えるときは、絶対的な速度だけでなく、ユーザーがどのように感じるのか、ユーザーが快適に操作できるかも考慮しなければならないが、このアプリではそれが出来ていない。</p>
<h2 id="トランジションによる解決">トランジションによる解決</h2>
<p>トランジションによって、この問題をある程度改善できる。</p>
<p>ラジオボタンをクリックしたとき、以下の 3 つの状態が変化している。</p>
<ol>
<li>selectedMember</li>
<li>headline</li>
<li>taskList</li>
</ol>
<p>このうち、処理に時間が掛かっているのは<code>taskList</code>のみである。他の 2 つは、状態更新そのものも、それに伴うレンダリングも、すぐに行える。<br/>
にも関わらず全てのレンダリングをまとめて行おうとするため、<code>taskList</code>に関する処理が終わるまで UI を全く更新できなくなってしまうのである。</p>
<p>既に述べたように、トランジションでは状態更新に優先順位をつけることができる。</p>
<p>今回の例だと、ラジオボタンが選択されたことはすぐに UI に反映させたいため、<code>selectedMember</code>は優先度が高い。<br/>
また、<code>headline</code>も処理に時間が掛かっているわけではないので、すぐに反映させることにする。<br/>
そして処理に時間が掛かっており、相対的に見て優先度や緊急性が低い<code>taskList</code>の状態更新を、トランジションとして扱うことにする。</p>
<p>まず、<code>startTransition</code>をインポートする。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> Fragment<span class="synStatement">,</span> useState<span class="synStatement">,</span> startTransition <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"react"</span><span class="synStatement">;</span>
</pre>
<p>そして、<code>startTransition</code>で<code>setTaskList</code>をラップする。<br/>
これにより、<code>setTaskList</code>はトランジションであると、React に伝えることができる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink> <span class="synType">const</span> handleChange <span class="synStatement">=</span> <span class="synStatement">(</span>e: ChangeEvent<span class="synStatement"><</span>HTMLInputElement<span class="synStatement">>)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> member <span class="synStatement">=</span> e.currentTarget.value <span class="synStatement">as</span> Member<span class="synStatement">;</span>
setSelectedMember<span class="synStatement">(</span>member<span class="synStatement">);</span>
setHeadline<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">${</span>member<span class="synSpecial">}</span><span class="synConstant">'s task`</span><span class="synStatement">);</span>
<span class="synComment">// startTransition で setTaskList をラップする</span>
startTransition<span class="synStatement">(()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
setTaskList<span class="synStatement">(()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
sleep<span class="synStatement">(</span><span class="synConstant">1500</span><span class="synStatement">);</span>
<span class="synStatement">return</span> allTasks.filter<span class="synStatement">((</span>t<span class="synStatement">)</span> <span class="synStatement">=></span> t.assignee <span class="synStatement">===</span> member<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">;</span>
</pre>
<p>この状態で再びサンプルアプリを触ってみる。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20220102/20220102220944.gif" width="412" height="500" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>ユーザーに操作に応じてラジオボタンと見出しが即座に更新されるようになった。</p>
<h2 id="useTransition-を使ったさらなる改善">useTransition を使ったさらなる改善</h2>
<p>だがまだ気になる点がある。メンバーを切り替えた際に、古いメンバーのタスクリストが表示され続けてしまっている。<br/>
これを改善し、選択したメンバーのタスクリストはまだ読込中であることをユーザーに伝えることができれば、より親切である。</p>
<p><code>useTransition</code>を使うことで、それを実現できる。</p>
<p>まず、<code>startTransition</code>ではなく<code>useTransition</code>をインポートする。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> Fragment<span class="synStatement">,</span> useState<span class="synStatement">,</span> useTransition <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"react"</span><span class="synStatement">;</span>
</pre>
<p>名前から類推できるように<code>useTransition</code>は Hooks なのだが、返り値の最初の要素には、<code>isPending</code>と呼ばれる真偽値が入っている。<br/>
次の要素は<code>startTransition</code>なので、これはそのまま使えばいい。</p>
<p><code>isPending</code>は、トランジションがレンダリングされている間は<code>true</code>になり、レンダリングが終わると<code>false</code>になる。<br/>
これを<code>TaskCardList</code>に渡すことで、読み込み中であることをユーザーに伝えることができる。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> App<span class="synStatement">()</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>selectedMember<span class="synStatement">,</span> setSelectedMember<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement"><</span>Member<span class="synStatement">>();</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>headline<span class="synStatement">,</span> setHeadline<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement"><</span><span class="synType">string</span><span class="synStatement">>(</span><span class="synConstant">""</span><span class="synStatement">);</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>taskList<span class="synStatement">,</span> setTaskList<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement"><</span>Task<span class="synIdentifier">[]</span><span class="synStatement">>(</span><span class="synIdentifier">[]</span><span class="synStatement">);</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>isPending<span class="synStatement">,</span> startTransition<span class="synIdentifier">]</span> <span class="synStatement">=</span> useTransition<span class="synStatement">();</span> <span class="synComment">// これを追加</span>
<span class="synComment">// 中略</span>
<span class="synStatement"><</span>TaskCardList taskList<span class="synStatement">=</span><span class="synIdentifier">{</span>taskList<span class="synIdentifier">}</span> isPending<span class="synStatement">=</span><span class="synIdentifier">{</span>isPending<span class="synIdentifier">}</span> /<span class="synStatement">></span> <span class="synComment">// isPending を渡す</span>
</pre>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// isPending を受け取るようにした</span>
<span class="synStatement">function</span> TaskCardList<span class="synStatement">(</span><span class="synIdentifier">{</span>
taskList<span class="synStatement">,</span>
isPending<span class="synStatement">,</span>
<span class="synIdentifier">}</span>: <span class="synIdentifier">{</span>
taskList: Task<span class="synIdentifier">[]</span><span class="synStatement">;</span>
isPending: <span class="synType">boolean</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>isPending<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement"><</span>div<span class="synStatement">></span>Loading...<span class="synStatement"><</span>/div<span class="synStatement">>;</span> <span class="synComment">// ペンディング中はその旨を表示する</span>
<span class="synStatement">return</span> taskList.length <span class="synStatement">></span> <span class="synConstant">0</span> ? <span class="synStatement">(</span>
<span class="synStatement"><</span>ul style<span class="synStatement">=</span><span class="synIdentifier">{{</span> listStyleType: <span class="synConstant">"none"</span> <span class="synIdentifier">}}</span><span class="synStatement">></span>
<span class="synIdentifier">{</span>taskList.map<span class="synStatement">((</span>task<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>li key<span class="synStatement">=</span><span class="synIdentifier">{</span>task.id<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>TaskCard <span class="synIdentifier">{</span>...task<span class="synIdentifier">}</span> /<span class="synStatement">></span>
<span class="synStatement"><</span>/li<span class="synStatement">></span>
<span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span>
<span class="synStatement"><</span>/ul<span class="synStatement">></span>
<span class="synStatement">)</span> : <span class="synType">null</span><span class="synStatement">;</span>
<span class="synIdentifier">}</span>
</pre>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/numb_86/20220102/20220102220835.gif" width="412" height="500" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://github.com/reactwg/react-18/discussions/4">Introducing React 18 · Discussion #4 · reactwg/react-18</a></li>
<li><a href="https://github.com/reactwg/react-18/discussions/41">New feature: startTransition · Discussion #41 · reactwg/react-18</a></li>
<li><a href="https://github.com/reactwg/react-18/discussions/65">Real world example: adding startTransition for slow renders · Discussion #65 · reactwg/react-18</a></li>
</ul>
numb_86