kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Next.js の Static HTML Export 機能などを学べる Next.js Learn (Excel) を試した

2月頃に Next.js を学ぶために Next.js Learn (Basic) を試した.詳しくは以下の記事にまとめてある.

kakakakakku.hatenablog.com

今回はその続きと言える Next.js Learn (Excel) を試して学んだことをまとめる.Basic と比較すると,一歩踏み込んだレベルだと思う.だからこそ Next.js Learn (Basic) とセットで終わらせておくと学習効率が1番高そう.僕自身,Excel を試すまでに少し間を空けてしまったことを後悔している.

nextjs.org

Next.js Learn (Excel)

Excel では「計4種類」のコンテンツが用意されている.正確には,最近まで Lazy Loading 関連のコンテンツもあったけど,Outdated であるという理由から2月末に削除されていた.なお,削除されたプルリクエストを見ると「Next.js チュートリアルを作り直している」という記載もあり,楽しみだ!気になる!

  • Export into a Static HTML App
  • TypeScript
  • Lazy Loading Modules ⚠️ 2月末に削除された
  • Lazy Loading Components ⚠️ 2月末に削除された
  • Create AMP Pages
  • Automatic Static Optimization

github.com

環境

今回は Next.js 9.3.2 を使えるようにローカル環境を構築した.Next.js Learn (Basic) を試した前回は Next.js 9.2.1 だった.

$ npm view next version
9.3.2

1. Export into a Static HTML App

まず「Export into a Static HTML App」では,Next.js でウェブアプリケーションを静的コンテンツとしてエクスポートできる Static HTML Export 機能を学ぶ.Next.js を動かすために Node.js 環境を作る必要がなく,例えば nginx さえあれば動かせるようになる.当然ながら,良し悪しはあるため,機能として知っておくことに価値がありそう.

nextjs.org

今回も Next.js Learn (Basic) で使ったリポジトリ zeit/next-learn-demo を使って,サンプルアプリケーションを動かす.

github.com

$ cd E1-static-export
$ npm install
$ npm run dev

npm run dev を実行すると,前回と同じ Batman TV Shows の画面にアクセスできる.現時点では,まだ普通の Next.js アプリケーションとなり,SSR (Server-Side Rendering) も使っている.画面遷移をしたり,URL 直接アクセスをすると,挙動は調べられる.

f:id:kakku22:20200330152734p:plain

次に next.config.js を以下のように実装する.関数から返却している paths には,エクスポートするパス情報を含めている.今回の例で言うと //about/show/[id] で,/show/[id] は実際に TVmaze API を実行した結果から自動的にエクスポートされる.

const fetch = require('isomorphic-unfetch');

module.exports = {
  exportTrailingSlash: true,
  exportPathMap: async function() {
    const paths = {
      '/': { page: '/' },
      '/about': { page: '/about' }
    };
    const res = await fetch('https://api.tvmaze.com/search/shows?q=batman');
    const data = await res.json();
    const shows = data.map(entry => entry.show);

    shows.forEach(show => {
      paths[`/show/${show.id}`] = { page: '/show/[id]', query: { id: show.id } };
    });

    return paths;
  }
};

そのまま npm run export を実行すると out ディレクトリにエクスポートされる.今回は軽量な HTTP サーバとして serve を使って配信すると,今までと同じ Batman TV Shows の画面にアクセスできる.

$ npm install -g serve

$ npm run export
(中略)
[==  ] Exporting (1/14)Fetched show: Batman
Show data fetched. Count: 10
Fetched show: The Batman
[==  ] Exporting (4/14)Fetched show: Batman Beyond
Fetched show: The Adventures of Batman
Fetched show: Batman: The Animated Series
[  ==] Exporting (7/14)Fetched show: Batman Unlimited
Fetched show: Batman: The Brave and the Bold
Fetched show: The New Batman Adventures
[ ===] Exporting (12/14)Fetched show: Beware the Batman
Fetched show: Batman: Black and White
Exporting (14/14)

$ cd out
$ serve -p 8080

実際に out ディレクトリを確認すると,HTML や JavaScirpt など,たくさんエクスポートされている.特に out/show を見ると,id ごとに index.html がある.next.config.js に実装した通り,エクスポート時に自動的に index.html を作っていると確認できる.

$ tree out/show
out/show
├── 11464
│   └── index.html
├── 22309
│   └── index.html
├── 33618
│   └── index.html
├── 3557
│   └── index.html
├── 481
│   └── index.html
├── 504
│   └── index.html
├── 5951
│   └── index.html
├── 757
│   └── index.html
├── 900
│   └── index.html
└── 975
    └── index.html

10 directories, 10 files

2. TypeScript

Next.js は TypeScript をサポートしている.特に Next.js 9 から,より便利に使えるようになった.

nextjs.org

検証用に新規ディレクトリを作り,React と Next.js と TypeScript をインストールする.

$ npm init -y
$ npm install --save react react-dom next
$ npm install --save-dev typescript @types/react @types/node

さらに pages/index.tsx を以下のように実装する.

const Home = () => <h1>Hello world!</h1>;

export default Home;

現時点だと,以下のような構成になっている.

$ tree -L 1
.
├── node_modules
├── package-lock.json
├── package.json
└── pages

2 directories, 2 files

ここで npm run dev を実行し,ローカルサーバを起動すると,なんと tsconfig.json を自動的に作ってくれる!最終的にカスタマイズは必要だとしても,Next.js のオススメ設定をベースに実装を進められるのは特に初学者にとって良さそう.

$ npm run dev

(中略)

[ wait ]  starting the development server ...
[ info ]  waiting on http://localhost:3000 ...
We detected TypeScript in your project and created a tsconfig.json file for you.

Your tsconfig.json has been populated with default values.

自動的に tsconfig.json が作られたよね?という驚きを確認するクイズもある.前回の記事にも書いたけど,手順通りに進めるだけではなく,動作確認をしながらクイズに回答する体験は Next.js Learn の良いところだと思う.

f:id:kakku22:20200330153117p:plain

なお,自動的に作られる tsconfig.json は,デフォルトだと "strict": false になっているため,ここを "strict": true にすると型エラーになる.既に型解決をした実装も用意されているため,以下のように変更するとうまく動く.最近は TypeScript を書く機会も増えているし,試せて良かった.

import { NextPage } from 'next';

const Home: NextPage<{ userAgent: string }> = ({ userAgent }) => (
  <h1>Hello world! - user agent: {userAgent}</h1>
);

Home.getInitialProps = async ({ req }) => {
  const userAgent = req ? req.headers['user-agent'] || '' : navigator.userAgent;
  return { userAgent };
};

export default Home;

3. Create AMP Pages

Next.js なら React アプリケーションを簡単に AMP に対応させることもできる(当然ながら Valid にするためには工夫も必要になる).「Create AMP Pages」では Next.js でサポートしている2種類の設定 amp: trueamp: 'hybrid' を試す.

nextjs.org

手順は今までとは違って,突然pages/index.jsamp: true を実装する」と書かれている.少し突き放されたような雰囲気もあるため,今回はさっき使った zeit/next-learn-demo リポジトリの E1-static-export ディレクトリを再利用することにした.まず pages/index.js を以下の実装に書き換えて,npm run dev を実行する.AMP の設定を config にしている.

export const config = { amp: true };

export default function Index(props) {
  return <p>Welcome to the AMP only Index page!!</p>;
}

Chrome 拡張機能「AMP Validator」を使って確認すると,data-ampdevmode 関連のエラーは1件出るけど,ちゃんと AMP に対応していた.具体的には,HTML の html タグに AMP 関連の設定が入っていた.

<html amp="" data-ampdevmode="" i-amphtml-layout="" i-amphtml-no-boilerplate="" transformed="self;v=1" amp-version="2003261442330" class="i-amphtml-singledoc i-amphtml-standalone" style="padding-top: 0px !important;">
(中略)
</html>

chrome.google.com

次に pages/index.js を以下の実装に変更し,AMP の設定を hybrid にする.http://localhost:3000/ にアクセスすると通常のレンダリングになり,http://localhost:3000/?amp=1 にアクセスすると AMP のレンダリングになる.isAmp() で判定できる.実際に使うなら hybrid で使うことが多そう.

import { useAmp } from 'next/amp';

export const config = { amp: 'hybrid' };

export default function Index(props) {
  const isAmp = useAmp();
  return <p>Welcome to the {isAmp ? 'AMP' : 'normal'} version of the Index page!!</p>;
}

4. Automatic Static Optimization

最後は Next.js 9 の新機能「Automatic Static Optimization」を試す.簡単に言うと,ビルドをするときに getInitialPropsgetStaticPaths など動的な実装があれば Server としてビルドし,完全に静的な実装なら Static にビルドしてくれる機能となる.ただし,Next.js 9.3 で getStaticProps など,Automatic Static Optimization を改善する機能がリリースされたことにより,コンテンツ内容と画像は古くなっていて,進めながら少し困った⚠️今回は Next.js 9.3 を前提に進める.

nextjs.org

まず,pages/index.js に以下の実装をして,npm run build を実行する.

const Index = () => <h1>Hello World</h1>;
export default Index;

すると,特に / に動的な実装はなく ○ (Static) と判断されていた.

$ npm run build
(中略)
Automatically optimizing pages

Page                                                           Size     First Load
┌ ○ /                                                          266 B       58.2 kB
└ ○ /404                                                       3.15 kB     61.1 kB
+ shared by all                                                57.9 kB
  ├ static/pages/_app.js                                       957 B
  ├ chunks/87dfcf810f5ac5b1c4a6c5a9e347b3d4582d9ddb.92878c.js  10.3 kB
  ├ chunks/framework.0f140d.js                                 40 kB
  ├ runtime/main.b9d743.js                                     5.95 kB
  └ runtime/webpack.b65cab.js                                  746 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)(Static)  automatically rendered as static HTML (uses no initial props)(SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

次に,新しく pages/agent.js に以下の実装をして,npm run build を実行する.これは getInitialProps を使ってレンダリング時に UserAgent を取得している.

const Agent = ({ userAgent }) => <h1>Your user agent is: {userAgent}</h1>;

Agent.getInitialProps = async ({ req }) => {
  const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;
  return { userAgent };
};

export default Agent;

すると,/agent に動的な実装があるため λ (Server) と判断されていた.

$ npm run build
(中略)
Automatically optimizing pages

Page                                                           Size     First Load
┌ ○ /                                                          266 B       58.5 kB
├ ○ /404                                                       3.15 kB     61.4 kB
└ λ /agent                                                     400 B       58.7 kB
+ shared by all                                                58.3 kB
  ├ static/pages/_app.js                                       959 B
  ├ chunks/842ebe18e826b71a6d34d074da5a709ac83a664a.79e44c.js  8.28 kB
  ├ chunks/db577d455de274a7f6e1386d96fe5570d4bd24c0.a3bfbe.js  2.4 kB
  ├ chunks/framework.0f140d.js                                 40 kB
  ├ runtime/main.ee8296.js                                     5.95 kB
  └ runtime/webpack.b65cab.js                                  746 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)(Static)  automatically rendered as static HTML (uses no initial props)(SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

最初はビルド時に表示される結果を見てもよくわからなかったけど,少しは理解できるようになった.Next.js Learn に感謝!

まとめ

Next.js Learn (Excel) を試した.Static HTML Export 機能や Automatic Static Optimization 機能を学べて,Next.js Learn (Basic) を試したときよりも,理解を一歩前進できたように思う.フィードバックとしては Next.js 9.3 を前提にコンテンツが更新されると良いかなと!とは言え,実際に Next.js を使ってアプリケーションを実装しないとチュートリアルレベルから抜け出せないため,プロトタイプを実装し始めた.Next.js をもっと学ぶぞ!