kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Mackerel : グラフアノテーションを登録するパターン集

新機能ではないけど,Mackerel の「グラフアノテーション機能」は便利でよく使っている.メトリクスの推移を分析するときに,同じタイミングで発生したイベント情報(デプロイ/キャンペーン開始/テレビ放送など)と紐付けることができる.実際にアノテーションを登録するときに「個人的によく使うパターン」がいくつかある.今回はパターンごとに「アノテーションを登録するスニペット」を整理しておく.

mackerel.io

コンソールを使う

アドホックにアノテーションを登録する場合は,1番簡単な「コンソール」を使う.とは言え,以下の画像のように「グラフの時刻部分をドラッグする」のは気付くにくく,Mackerel 初学者には使ってもらえなさそう.

f:id:kakku22:20200225115147p:plain

mkr コマンドを使う

シェルからアノテーションを登録する場合は,mkr コマンドを使う.mkr annotations を使うと簡単に登録できる.--from--to に指定するタイムスタンプは「UNIX 時間(エポック秒)」にする.

$ mkr annotations create --title 'アノテーション (Title) from mkr' \
  --description 'アノテーション (Description) from mkr' \
  --from $(date +%s) \
  --to $(date +%s) \
  --service 'xxxxx'

github.com

f:id:kakku22:20200225113343p:plain

Mackerel Client を使う

Ruby と Go で実装したアプリケーションからアノテーションを登録する場合は「Mackerel Client」を使う.例えば,mackerel-client-rubymackerel-client-go はグラフアノテーションをサポートしている(他の Client も使えると思う).以下は mackerel-client-rubypost_graph_annotation() を使っている.

require 'mackerel-client'
require 'time'

client = Mackerel::Client.new(mackerel_api_key: ENV['MACKEREL_API_KEY'])

client.post_graph_annotation(
  {
    title: 'アノテーション (Title) from Ruby Client',
    description: 'アノテーション (Description) from Ruby Client',
    from: Time.now.to_i,
    to: Time.now.to_i,
    service: 'xxxxx'
  }
)

github.com

f:id:kakku22:20200225113404p:plain

Mackerel API を使う

Python で実装したアプリケーションからアノテーションを登録する場合は「Mackerel API」を直接 requests で実行して使う.

import json
import os
import requests
import time

params = {
    'title': 'アノテーション (Title) from API',
    'description': 'アノテーション (Description) from API',
    'from': int(time.time()),
    'to': int(time.time()),
    'service': 'xxxxx'
}

requests.post(
    'https://api.mackerelio.com/api/v0/graph-annotations',
    json.dumps(params),
    headers={'Content-Type': 'application/json', 'X-Api-Key': os.environ['MACKEREL_API_KEY']}
)

mackerel.io

f:id:kakku22:20200225113425p:plain

まとめ

完全網羅を目指すのではなく,個人的によく使う以下のパターンに限定して「グラフアノテーションを登録するスニペット」を整理した.今まで何度も同じ検索を繰り返していたため,今後アノテーションを使うときは kakakakakku blog を見るぞ!便利!

  • アドホックなら「コンソール」を使う
  • シェルならmkr コマンド」を使う
  • Ruby と Go なら「Mackerel Client」を使う
  • Python なら「Mackerel API」を使う

Jupyter Notebook でパフォーマンスを計測するなら %%timeit と書こう

Python で実装した処理のパフォーマンスを計測するときに,たまに timeit を使っている.timeit は処理を繰り返し実行することにより,精緻な計測結果を把握できる.今までは timeitimport して直接実行していたけど,よく調べてみると,Jupyter Notebook (IPython) で「マジックコマンド」として %%timeit と書けることを最近知った.import も必要なく使えて便利だった!

ipython.readthedocs.io

%%time を使う

実行回数は「ループ数」「繰り返し数」から決まる.%%timeit にオプションを指定せず実行すると「ループ数」は適切な精度が得られるように自動的に決まる仕組みになっている.なお「繰り返し数」はデフォルト 7 となる.Jupyter Notebook で簡単なサンプルコードを実装し,セルの頭に %%timeit と書いて実行すると,以下のようになる.mean ± std と書いてあるため「平均実行時間」「標準偏差」だとわかる.なお「ループ数」1000000 になっていた.

%%timeit
sum(range(100))
1.42 µs ± 48.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%time -n を使う

%%timeit にオプション -n を指定すると「ループ数」を固定できる.今回は 10 に固定して実行した.

%%timeit -n 10
sum(range(100))
1.25 µs ± 30 ns per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%time -n -r を使う

%%timeit にオプション -r を指定すると「繰り返し数」を固定できる.今回は 2 に固定して実行した.

%%timeit -n 10 -r 2
sum(range(100))
1.28 µs ± 66.9 ns per loop (mean ± std. dev. of 2 runs, 10 loops each)

Jupyter Notebook と VS Code

以下の記事に書いた通り,最近は Jupyter Notebook を実装するときに Visual Studio Code (VS Code) を使う機会が増えている.

kakakakakku.hatenablog.com

今回の %%timeit の検証も VS Code を使っている.

f:id:kakku22:20200224232058p:plain

まとめ

Jupyter Notebook でパフォーマンスを計測するときは「マジックコマンド」として %%timeit を使うと便利だった.オプションにより「ループ数」「繰り返し数」を固定できるけど,デフォルトで適切な精度が得られるように実行されるため,あまり気にしなくて良さそうに思う.

MySQL のサンプルデータセット "world" データベース と "world_x" データベースの差とは?

MySQL 関連の検証をしたり,データベース未経験者に SQL を教えたりするときに,よく MySQL 公式の「world データベース」を使っている.「国と都市と言語」を対象にしたデータセットとなり,とても使いやすいと思う.例えばRedash ハンズオンでも使っている.

Example Databases

MySQL 公式のデータセットは他にもある.個人的に簡単に試すなら worldを使って,ある程度の規模を必要とするなら employeesakila を使っている.

  • employee data(従業員データ)
  • world database(国データ)
  • world_x database(国データ)
  • sakila database(DVD レンタルショップデータ)
  • menagerie database(ペットデータ)

dev.mysql.com

world と world_x の差は?

今まで「world_x データベース」を使ったことがなく,そもそも worldworld_x の差を把握できていなかった.今回は world_x を調べた結果をまとめる.なお,MySQL 8.0.19 を検証環境にした.

結論から整理すると,データサイズに差はなく,スキーマに差がある.具体的には world_x データベースでは一部のデータが「JSON 型」で,Document Store として使えるようになっている.よって,MySQL の「X DevAPI」を試せる環境となり,データベース名も world_x になっていると推測できる.やはり,ハンズオンなど簡単に使うなら今後も world で良さそう.なお,テーブル情報の概要は以下に載っている.

テーブル

まず,worldworld_x のテーブルを確認する.world「計3テーブル」となり,world_x テーブルは「計4テーブル」となる.とは言え,どちらも「国」「都市」「言語」のデータを持っている.

mysql> SHOW TABLES FROM world;
+-----------------+
| Tables_in_world |
+-----------------+
| city            |
| country         |
| countrylanguage |
+-----------------+
3 rows in set (0.01 sec)

mysql> SHOW TABLES FROM world_x;
+-------------------+
| Tables_in_world_x |
+-------------------+
| city              |
| country           |
| countryinfo       |
| countrylanguage   |
+-------------------+
4 rows in set (0.01 sec)

レコード

次に,worldworld_x のレコード数を確認する.既に書いた通り,データサイズに差はなかった.

  • world データベース
    • city : 4079 レコード
    • country : 239 レコード
    • countrylanguage : 984 レコード
  • world_x データベース
    • city : 4079 レコード
    • country : 239 レコード
    • countryinfo : 239 レコード
    • countrylanguage : 984 レコード

SQL も残しておく.

-- world
SELECT COUNT(*) FROM world.city;
SELECT COUNT(*) FROM world.country;
SELECT COUNT(*) FROM world.countrylanguage;

-- world_x
SELECT COUNT(*) FROM world_x.city;
SELECT COUNT(*) FROM world_x.country;
SELECT COUNT(*) FROM world_x.countryinfo;
SELECT COUNT(*) FROM world_x.countrylanguage;

スキーマ

SHOW COLUMNS を使って worldworld_x のスキーマを確認する.

world は一般的なスキーマとなり,city.CountryCodecountrylanguage.CountryCodecountry.Code の外部キーになっている.個人的にも使い慣れたスキーマと言える.

world スキーマ

mysql> SHOW COLUMNS FROM world.city;
+-------------+----------+------+-----+---------+----------------+
| Field       | Type     | Null | Key | Default | Extra          |
+-------------+----------+------+-----+---------+----------------+
| ID          | int      | NO   | PRI | NULL    | auto_increment |
| Name        | char(35) | NO   |     |         |                |
| CountryCode | char(3)  | NO   | MUL |         |                |
| District    | char(20) | NO   |     |         |                |
| Population  | int      | NO   |     | 0       |                |
+-------------+----------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world.country;
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
| Field          | Type                                                                                  | Null | Key | Default | Extra |
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
| Code           | char(3)                                                                               | NO   | PRI |         |       |
| Name           | char(52)                                                                              | NO   |     |         |       |
| Continent      | enum('Asia','Europe','North America','Africa','Oceania','Antarctica','South America') | NO   |     | Asia    |       |
| Region         | char(26)                                                                              | NO   |     |         |       |
| SurfaceArea    | decimal(10,2)                                                                         | NO   |     | 0.00    |       |
| IndepYear      | smallint                                                                              | YES  |     | NULL    |       |
| Population     | int                                                                                   | NO   |     | 0       |       |
| LifeExpectancy | decimal(3,1)                                                                          | YES  |     | NULL    |       |
| GNP            | decimal(10,2)                                                                         | YES  |     | NULL    |       |
| GNPOld         | decimal(10,2)                                                                         | YES  |     | NULL    |       |
| LocalName      | char(45)                                                                              | NO   |     |         |       |
| GovernmentForm | char(45)                                                                              | NO   |     |         |       |
| HeadOfState    | char(60)                                                                              | YES  |     | NULL    |       |
| Capital        | int                                                                                   | YES  |     | NULL    |       |
| Code2          | char(2)                                                                               | NO   |     |         |       |
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
15 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world.countrylanguage;
+-------------+---------------+------+-----+---------+-------+
| Field       | Type          | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+-------+
| CountryCode | char(3)       | NO   | PRI |         |       |
| Language    | char(30)      | NO   | PRI |         |       |
| IsOfficial  | enum('T','F') | NO   |     | F       |       |
| Percentage  | decimal(4,1)  | NO   |     | 0.0     |       |
+-------------+---------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

MySQL Workbench で生成した ER も載せておく.

f:id:kakku22:20200217102133p:plain

world_x スキーマ

world_xcity.Infocountryinfo.doc など,「JSON 型」のカラムを持ったテーブルがある.MySQL を Document Store として使う検証環境として最適だと思う.

mysql> SHOW COLUMNS FROM world_x.city;
+-------------+----------+------+-----+---------+----------------+
| Field       | Type     | Null | Key | Default | Extra          |
+-------------+----------+------+-----+---------+----------------+
| ID          | int      | NO   | PRI | NULL    | auto_increment |
| Name        | char(35) | NO   |     |         |                |
| CountryCode | char(3)  | NO   |     |         |                |
| District    | char(20) | NO   |     |         |                |
| Info        | json     | YES  |     | NULL    |                |
+-------------+----------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world_x.country;
+---------+----------+------+-----+---------+-------+
| Field   | Type     | Null | Key | Default | Extra |
+---------+----------+------+-----+---------+-------+
| Code    | char(3)  | NO   | PRI |         |       |
| Name    | char(52) | NO   |     |         |       |
| Capital | int      | YES  |     | NULL    |       |
| Code2   | char(2)  | NO   |     |         |       |
+---------+----------+------+-----+---------+-------+
4 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world_x.countryinfo;
+--------------+---------------+------+-----+---------+-------------------+
| Field        | Type          | Null | Key | Default | Extra             |
+--------------+---------------+------+-----+---------+-------------------+
| doc          | json          | YES  |     | NULL    |                   |
| _id          | varbinary(32) | NO   | PRI | NULL    | STORED GENERATED  |
| _json_schema | json          | YES  |     | NULL    | VIRTUAL GENERATED |
+--------------+---------------+------+-----+---------+-------------------+
3 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world_x.countrylanguage;
+-------------+---------------+------+-----+---------+-------+
| Field       | Type          | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+-------+
| CountryCode | char(3)       | NO   | PRI |         |       |
| Language    | char(30)      | NO   | PRI |         |       |
| IsOfficial  | enum('T','F') | NO   |     | F       |       |
| Percentage  | decimal(4,1)  | NO   |     | 0.0     |       |
+-------------+---------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

MySQL Workbench で生成した ER も載せておく.テーブル定義を確認すると world_x.city には外部キーの指定がないこともわかる.

f:id:kakku22:20200217102237p:plain

なお,「JSON 型」 だとデータ構造を確認しにくいため,実際のデータを JSON_PRETTY() で載せておく.city.Info はシンプルに Population(人口) のみとなる.countryinfo.docName(国名)government(政府情報)geography(地理情報) など,様々な情報が JSON に含まれている.

mysql> SELECT JSON_PRETTY(Info) FROM world_x.city WHERE CountryCode = 'JPN' AND NAME = 'Tokyo';
+-----------------------------+
| JSON_PRETTY(Info)           |
+-----------------------------+
| {
  "Population": 7980230
} |
+-----------------------------+
1 row in set (0.01 sec)

mysql> SELECT JSON_PRETTY(doc) FROM world_x.countryinfo WHERE JSON_EXTRACT(doc, '$.Code') = 'JPN' LIMIT 1;
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| JSON_PRETTY(doc)                                                                                                                                                                                                                                                                                                                                                                                                                 |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {
  "GNP": 3787042,
  "_id": "00005de917d8000000000000006d",
  "Code": "JPN",
  "Name": "Japan",
  "IndepYear": -660,
  "geography": {
    "Region": "Eastern Asia",
    "Continent": "Asia",
    "SurfaceArea": 377829
  },
  "government": {
    "HeadOfState": "Akihito",
    "GovernmentForm": "Constitutional Monarchy"
  },
  "demographics": {
    "Population": 126714000,
    "LifeExpectancy": 80.69999694824219
  }
} |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)

world_x クエリ : 東京都の Population(人口) を取得する

最後にクエリサンプルを載せておく.

まず,city テーブルから東京都の Population(人口) を取得する.今回は -> オペレータを使う.

mysql> SELECT Info->'$.Population' FROM world_x.city WHERE CountryCode = 'JPN' AND Name = 'Tokyo';
+----------------------+
| Info->'$.Population' |
+----------------------+
| 7980230              |
+----------------------+
1 row in set (0.01 sec)

world_x クエリ : 日本の Region(地域)Population(人口) を取得する

今度は日本の Region(地域)Population(人口) を取得する.world_x の場合はテーブルが分割されているため,country テーブルと countryinfo テーブルを JOIN する必要がある.今回は -> オペレータと ->> オペレータを使う.通常の char と JSON の中にある文字列を ON で紐付けた.

mysql> SELECT ci.doc->>'$.Code', ci.doc->'$.demographics.Population', c.Capital FROM world_x.country c INNER JOIN world_x.countryinfo ci ON c.Code = ci.doc->>'$.Code' WHERE c.Name = 'Japan';
+----------------------------------------------+---------------------------------------------------+---------+
| JSON_UNQUOTE(JSON_EXTRACT(c2.doc, '$.Code')) | JSON_EXTRACT(c2.doc, '$.demographics.Population') | Capital |
+----------------------------------------------+---------------------------------------------------+---------+
| JPN                                          | 126714000                                         |    1532 |
+----------------------------------------------+---------------------------------------------------+---------+
1 row in set (0.01 sec)

なお,JSON_EXTRACT()->JSON_UNQUOTE(JSON_EXTRACT())->> の話は昨日の記事にまとめてある.

kakakakakku.hatenablog.com

まとめ

MySQL 公式のデータセット worldworld_x の差を把握するために検証環境を構築して調べた.今後も SQL を教えたりする簡単な場面では world を使おうと思う.もし「JSON 型」などを検証する必要があったら world_x を使おうと思う.差を把握できて良かった!

MySQL で JSON 型からクオートを除去した文字列を取得するなら ->> を使う

MySQL(5.7 以降)で「JSON 型」のカラムから指定したキーを取得する場合 JSON_EXTRACT() を使う.もしくは JSON_EXTRACT() と同じ動作をする -> オペレータを使うこともできる.以下に members テーブルの info カラムから name キーを取得する SQL を載せる.なお,今回は MySQL 8.0.19 を検証環境にした.

-- サンプルテーブルを作成する
mysql> CREATE TABLE members ( info json DEFAULT NULL );
Query OK, 0 rows affected (0.10 sec)

-- サンプルデータを追加する
mysql> INSERT INTO members VALUES ('{"id": 1, "name": "kakakakakku"}');
Query OK, 1 row affected (0.10 sec)

-- サンプルデータを確認する
mysql> SELECT * FROM members;
+----------------------------------+
| info                             |
+----------------------------------+
| {"id": 1, "name": "kakakakakku"} |
+----------------------------------+
1 row in set (0.01 sec)

-- JSON_EXTRACT() を使って取得する
mysql> SELECT JSON_EXTRACT(info, '$.name') FROM members;
+------------------------------+
| JSON_EXTRACT(info, '$.name') |
+------------------------------+
| "kakakakakku"                |
+------------------------------+
1 row in set (0.01 sec)

-- -> を使って取得する
mysql> SELECT info->'$.name' FROM members;
+----------------+
| info->'$.name' |
+----------------+
| "kakakakakku"  |
+----------------+
1 row in set (0.01 sec)

JSON_UNQUOTE() と組み合わせる

実際に JSON_EXTRACT() を使って文字列を取得すると "kakakakakku" のように「ダブルクオート付き」になる.例えば CHARVARCHAR と比較するときに困るため,JSON_UNQUOTE() と組み合わせるとクオートを除去できる.

-- JSON_UNQUOTE(JSON_EXTRACT()) を使って取得する
mysql> SELECT JSON_UNQUOTE(JSON_EXTRACT(info, '$.name')) FROM members;
+--------------------------------------------+
| JSON_UNQUOTE(JSON_EXTRACT(info, '$.name')) |
+--------------------------------------------+
| kakakakakku                                |
+--------------------------------------------+
1 row in set (0.01 sec)

->> オペレータを使う

JSON_UNQUOTE(JSON_EXTRACT()) を使うと JSON_UNQUOTE(JSON_EXTRACT(info, '$.name')) のように長くなってしまう.箇所が多いと SQL の可読性に影響する可能性もある.そこで JSON_UNQUOTE(JSON_EXTRACT()) と同じ動作をする ->> オペレータを使うこともできる.MySQL 5.7.13 で導入された ->> オペレータの存在は今まで気付いていなかった!

-- ->> を使って取得する
mysql> SELECT info->>'$.name' FROM members;
+-----------------+
| info->>'$.name' |
+-----------------+
| kakakakakku     |
+-----------------+
1 row in set (0.01 sec)

ドキュメントを読むと ->>equivalent to JSON_UNQUOTE(JSON_EXTRACT()) と書いてある.

名前 説明
-> Return value from JSON column after evaluating path; equivalent to JSON_EXTRACT().
->> Return value from JSON column after evaluating path and unquoting the result; equivalent to JSON_UNQUOTE(JSON_EXTRACT()).

dev.mysql.com

まとめ

  • MySQL で「JSON 型」のカラムから指定したキーを取得する場合は JSON_EXTRACT()JSON_UNQUOTE() を組み合わせる
  • JSON_UNQUOTE(JSON_EXTRACT()) と同じ動作をする ->> オペレータを使うと短く書ける

関連記事

4年前に JSON_EXTRACT() など JSON を操作する関数を調査した記事も参考になった.とは言え,MySQL 5.7 も MySQL 8.0 も JSON を操作する関数が増えているため,もう1度調査をしておくと良さそう.

kakakakakku.hatenablog.com

Next.js Learn (Basic) を試して学んだ Next.js の基礎の基礎

React を使ってプロトタイプを実装したりすることはあるけど,今まで Next.js を試したことがなかった.そろそろ入門しなければ!という危機感もあり,Next.js Learn (Basic) を一通り試してみた.入門する前と比較すると「Next.js でできること(基礎の基礎)」を把握することができ,非常に良質なコンテンツに仕上がっていた.今回は Next.js Learn (Basic) から学んだことを整理する.Next.js に入門するならまず試してみると良いと思う.

nextjs.org

なお,今回は Next.js 9.2.1 を使えるようにローカル環境を構築した.

$ npm view next version
9.2.1

Next.js Learn (Basic)

Basic では「計9種類」のコンテンツが用意されている.Next.js を使ったサイト内遷移(ルーティング)もあれば,共通コンポーネントの設計もある.ページを静的に最適化したり,styled-jsx を使った CSS-in-JS を実現したり,Now を使ったデプロイも学べる.SSR (Server-Side Rendering) 以外の機能も多く試せる.素晴らしい!

  • Getting Started
  • Navigate Between Pages
  • Using Shared Components
  • Create Dynamic Pages
  • Clean URLs with Dynamic Routing
  • Fetching Data for Pages
  • Styling Components
  • API Routes
  • Deploying a Next.js App

また,手順通りに進めるだけではなく,以下のように動作確認をしながら結果を選ぶクイズも出る.写経をしながら進める価値もあるし,理解度も高まるし,よく考えられているなと思う.

f:id:kakku22:20200206142229p:plain

Next.js アプリケーション

Next.js Learn (Basic) のコンテンツは全て繋がっている.ステップバイステップに学ぶことができるし,コンテンツごとに実装する Next.js アプリケーションも異なる.具体的には,以下の「計4種類」の Next.js アプリケーションを実装する.楽しいぞ!

  • Hello World
  • Blog
  • TV Shows(番組表)
  • Famous Quotes(名言集)

参考までに「Blog アプリ」「TV Shows アプリ」のキャプチャを載せておく.

f:id:kakku22:20200206142252p:plain

f:id:kakku22:20200206142307p:plain

コードは GitHub に公開されている.

github.com

HMR (Hot Module Replacement)

Next.js では,開発環境だとデフォルトで HMR (Hot Module Replacement) が使える.コードを修正すると,すぐにページに反映されるため,実装時のストレスがなく,便利だった.

Link コンポーネント

Next.js では pages ディレクトリ直下に JavaScript ファイルを置くと,自動的に認識される.例えば pages/about.js を作成すれば http://localhost:3000/about にアクセスできるようになる.そして,サイト内遷移を実現するときには Link コンポーネントを使う.今回は「Hello World アプリ」で以下のようなコードを実装した.react-router-dom などを使わずに Next.js で書けるのは良かった.

import Link from 'next/link';

export default function Index() {
  return (
    <div>
      <Link href="/about">
        <a title="About Page">About Page</a>
      </Link>
      <p>Hello Next.js</p>
    </div>
  );
}

Create Dynamic Pages

「Blog アプリ」で2種類の「サイト内遷移」を実装する.最初は以下のような実装となり,Link コンポーネントを使って href props にクエリストリングを追加している.

import Layout from '../components/MyLayout.js';
import Link from 'next/link';

const PostLink = props => (
  <li>
    <Link href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
);

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        <PostLink title="Hello Next.js" />
        <PostLink title="Learn Next.js is awesome" />
        <PostLink title="Deploy apps with Zeit" />
      </ul>
    </Layout>
  );
}

遷移したページでは以下のような実装となり,useRouter フックを使ってクエリストリングを取得している.

import { useRouter } from 'next/router'
import Layout from '../components/MyLayout.js'

const Page = () => {
  const router = useRouter()

  return (
    <Layout>
      <h1>{router.query.title}</h1>
      <p>This is the blog post content.</p>
    </Layout>
  )
}

export default Page

ただし,この実装だと URL は http://localhost:3000/post?title=Hello%20Next.js となり,課題が残る.そこで,Next.js の「Dynamic Routes」機能を使う.以下のような実装となり,href props は /p/[id] のように [] を使った記法になる.また as を使って [id] の値を「動的に」設定する.

import Layout from '../components/MyLayout.js'
import Link from 'next/link'

const PostLink = props => (
  <li>
    <Link href="/p/[id]" as={`/p/${props.id}`}>
      <a>{props.id}</a>
    </Link>
  </li>
)

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        <PostLink id="hello-nextjs" />
        <PostLink id="learn-nextjs" />
        <PostLink id="deploy-nextjs" />
      </ul>
    </Layout>
  )
}

遷移したページの実装はあまり変わらないけど,ファイルパスが pages/p/[id].js となり,[] をそのままファイル名に使う点は新しく感じた.URL は http://localhost:3000/p/hello-nextjs となる.

pages
├── about.js
├── index.js
└── p
    └── [id].js

getInitialProps を使う

「TV Shows アプリ」を実装するときに,TVmaze API を使ってテレビ番組データを取得する.そのため,初回ロード時に API を実行する必要があり,今回は getInitialProps を使った.例えば,以下のように実装すると,Index コンポーネントの props から shows を取得できるようになる.

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman');
  const data = await res.json();

  console.log(`Show data fetched. Count: ${data.length}`);

  return {
    shows: data.map(entry => entry.show)
  };
};

次にテレビ番組データを表示するページの実装をする.以下の実装に console.log があり,このログ出力を動作確認する手順は個人的には気付きが多かった.この console.log のログは「ブラウザ側」「サーバ側」のどちらに出力されるのか?

import Layout from '../../components/MyLayout';
import fetch from 'isomorphic-unfetch';

const Post = props => (
  <Layout>
    <h1>{props.show.name}</h1>
    <p>{props.show.summary.replace(/<[/]?[pb]>/g, '')}</p>
    {props.show.image ? <img src={props.show.image.medium} /> : null}
  </Layout>
);

Post.getInitialProps = async function(context) {
  const { id } = context.query;
  const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
  const show = await res.json();

  console.log(`Fetched show: ${show.name}`);

  return { show };
};

export default Post;

結果としては,トップページから画面遷移をすると「ブラウザ側」にログが表示される.そして http://localhost:3000/p/481 のように URL を直接開くと「サーバ側」にログが表示される.この挙動こそ SSR だよなー!と改めて気づけて良かった.

まとめ

  • Next.js に入門するために Next.js Learn (Basic) を一通り試した
  • Next.js でできることを把握することができる
  • 次は Next.js Learn (Excel) に進むぞ!学ぶことが無限にある!

コンテンツとステップ

最後は付録として,コンテンツごとにステップをまとめておく.多くあるように見えるけど,実際に写経(とコピペ)をしながら進めるだけなら「2,3時間」あれば十分に終わると思う.参考までー!

  • Getting Started
    • Introduction
    • Setup
    • 404 Page
    • Creating Our First Page
    • Handling Errors
    • You are Awesome
  • Navigate Between Pages
    • Introduction
    • Setup
    • Using Link
    • Client-Side History Support
    • Adding Link Props
    • Link is Just a Wrapper Component
    • Link is Simple, but Powerful
  • Using Shared Components
    • Introduction
    • Setup
    • Create the Header Component
    • Using the Header Component
    • The Component Directory
    • The Layout Component
    • Rendering Child Components
    • Using Components
  • Create Dynamic Pages
    • Introduction
    • Setup
    • Adding a list of posts
    • Passing Data via Query Strings
    • useRouter
    • Finally
  • Clean URLs with Dynamic Routing
    • Introduction
    • Setup
    • Dynamic Routing
    • History Awareness
    • Finally
  • Fetching Data for Pages
    • Introduction
    • Setup
    • Fetching Batman Shows
    • Only on the Server
    • Implement the Post Page
    • Fetch Data in Client Side
    • Finally
  • Styling Components
    • Introduction
    • Setup
    • Styling our home page
    • Styles should go inside template strings
    • Styles and Nested Components
    • No Effect for Nested Component
    • Global Styles
    • Global Styles Work
    • What Next
  • API Routes
    • Introduction
    • Setup
    • Creating an API route
    • Fetching API Routes
    • Middlewares
    • Finally
  • Deploying a Next.js App
    • Introduction
    • Setup
    • Deploying to ▲ZEIT Now
    • Deploying to Your Own Environment
    • Build and Start
    • Run two instances
    • Build Once, Run Many Instances
    • Finally