Open10

Nextjs×Typescriptで始めるJAMstackブログ

awakeiawakei

はじめに

やりたいこと

  • 自分自身のブログページ(というより個人ページ?)を作りたい
  • react使ってみたいのでNextjsで
  • Typescriptも使ってみる
  • JAMstack
  • 動かす先はvercel
awakeiawakei

プロジェクト作成と初期設定(その1)

create-next-app

まずは、プロジェクトを作成する。
create-next-appwith-typescriptオプションを利用して作成します。

$ npx create-next-app --example with-typescript my_blog

とりあえず起動してみる

$ yarn dev

質素ですが、起動しました。

eslintとprettierの設定

古い記憶でtslintなるものがあった気がしたのですが、どうやらeslintに大統一されたようなのでeslintを入れます。あとvscodeで開発してると便利なのでprettierも。

必要パッケージのインストール

何入れればいいのかよくわからない部分も多く、記事を参考させていただきました。
ただ大まかに分類すると以下のような感じです。
※分けてますが、一括でaddしても大丈夫です。

eslint × react

  • eslint
  • eslint-plugin-react
  • eslint-plugin-react-hooks

eslint × typescript

  • @typescript-eslint/parser
  • @typescript-eslint/eslint-plugin

prettier × eslint

  • prettier
  • eslint-config-prettier
$ yarn add -D eslint eslint-plugin-react eslint-plugin-react-hooks
$ yarn add -Dprettier eslint-config-prettier
$ yarn add -D @typescript-eslint/{parser,eslint-plugin}

スクリプトの追加

各種インストール後の package.json はこんな感じです。
あと、あとからlintのスクリプトを実行しやすいように登録しておきます。

package.json
{
  "name": "with-typescript",
  "version": "1.0.0",
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "type-check": "tsc",
    "lint": "eslint . --ext .ts,.tsx" // ここにlintコマンド追加
  },
  "dependencies": {
    "next": "latest",
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  },
  "devDependencies": {
    "@types/node": "^12.12.21",
    "@types/react": "^16.9.16",
    "@types/react-dom": "^16.9.4",
    "@typescript-eslint/eslint-plugin": "^4.8.1",
    "@typescript-eslint/parser": "^4.8.1",
    "eslint": "^7.14.0",
    "eslint-config-prettier": "^6.15.0",
    "eslint-plugin-react": "^7.21.5",
    "eslint-plugin-react-hooks": "^4.2.0",
    "prettier": "^2.2.0",
    "typescript": "4.0"
  },
  "license": "MIT"
}

.eslintrc.json / .prettierrc.json およびvscodeの設定

各種設定ファイルは下記を参考にしたので直接ご覧ください。
(ちゃんと完全に理解して書けるようになりたい…)
https://qiita.com/sprout2000/items/ee4fc97f83f45ba1d227

一度実行

$ yarn lint

実行したのですがたくさんエラーでてきた。。。
ひとまずeslintが動いたので次から直していきたいと思います。

参考

awakeiawakei

プロジェクト作成と初期設定(その2)

eslintを直す

前スレッドの最後で大量にeslintに怒られたのですが、大きくは次の二つでした。

  • 【error】'React' must be in scope when using JSX react/react-in-jsx-scope
  • 【warning】 Missing return type on function @typescript-eslint/explicit-module-boundary-types

なのでそちらをそれぞれ調べながら修正していきます。

'React' must be in scope when using JSX

こちらですが、以下のようなファイルで出ています。

components/index.tsx
import Link from 'next/link'
import Layout from '../components/Layout'

const IndexPage = () => (
  <Layout title="Home | Next.js + TypeScript Example">
    <h1>Hello Next.js 👋</h1>
    <p>
      <Link href="/about">
        <a>About</a>
      </Link>
    </p>
  </Layout>
)

export default IndexPage

vscodeだとこんな感じ

いわれた通り、以下を追加すると通るようになりました。

import React from 'react';

が、しかし、ReactをimportするのはLayoutファイルでやっているため、今回はoffにしてもよさそう。

.eslintrc.json
"rules": {
  "react/prop-types": "off",
  "react/react-in-jsx-scope": "off"
},

(もっと良い対応方法があれば知りたいです)

Missing return type on function

直訳すると関数の返り値にも型をつけなさい、とのこと。(そりゃそうだ)
warningだけど直していきたいと思います。
初めは以下のような感じですが、初期でwarningがでているのはcomponentのreturnのとこなので、ReactElement を読み込んで、ReactElementと書いていきます。

components/index.tsx
import Link from 'next/link'
import Layout from '../components/Layout'
import { ReactElement } from 'react'; // これも追加

//  const IndexPage = () => (
const IndexPage = (): ReactElement => (
  <Layout title="Home | Next.js + TypeScript Example">
    <h1>Hello Next.js 👋</h1>
    <p>
      <Link href="/about">
        <a>About</a>
      </Link>
    </p>
  </Layout>
)

export default IndexPage

ひとまずこちらの修正もしたらlintの結果もきれいになりました!
次回はtypescriptとreactそのものも理解しながら進めていきたいと思います。

awakeiawakei

Next.jsのチュートリアル

「あれ、そういえばnextのことなんにもわからなくね?」となったので一旦脱線して、Next.jsのチュートリアルをやります。

こちらを使います。
公式だし、ステップごとに分けられておりわかりやすいので普通にこれでいいと思います。
https://nextjs.org/learn/basics/create-nextjs-app

Create a Next.js App ~ Navigate Between Pages

ここらへんはチュートリアルアプリの作成と簡単なページの編集、内部のルーティングの話です。
nuxtを知っているので特に引っかかることもなかったです。
Pagesにファイル作ると勝手にルーティングしてくれるのはとても良いですね。
正直これだけでも使用価値がある気がします。

Assets, Metadata, and CSS

まず、image系のファイルに関して。publicに入れておけば、以下のようにルートからアクセスできますよ、とのこと。(この場合はpublic/vercel.svg

<img src="/vercel.svg" alt="Vercel Logo" className="logo" />

次はメタデータに関して。SEOとかやってるとここら辺かなり大事ですね。
Headはnextのモジュールの中にすでにあって、ページコンポーネントでimportして使える。
それを呼び出して、titleとかを設定できるとのこと。
地味に驚きで、railsでいう application.html.erb みたいなlayoutファイルを自前で定義するのかな、と思いきや、メタタグ周りを公式でモジュール化しているのかと。
ブラックボックスになっている感はありつつも、変に設定をミスることもなさそうで初心者にはやさしいかも。

また、

If you want to customize the <html>, for example to add the lang attribute, you can do so by creating a pages/_document.js file. Learn more in the custom Document documentation.

とのことで、langのようなhtmlの属性(?)をいじりたかったら pages/_document.js を作ってカスタムせよ、と。ここも上と同じ理由で地味に驚きでした。

あまり進まなかったので、次はCSSから。

awakeiawakei

Next.jsのチュートリアル(その2)

前回の続きでCSSからやります。

Assets, Metadata, and CSS

CSS

このような書き方はsytled-jsxという物を使っているらしい。標準なのかな?

<style jsx>{`
  …
`}</style>

とはいえそれ以外にもいろいろ推奨ライブラリがあるっぽいですね。
ただ、vueに比べて管理がしづらいという欠点を小耳に挟んだ気がするので、CSSの運用方法は別途深掘りする必要がありそう。

個人的にはvueみたいにscopeが切られててstylusとかscssが使えるととても嬉しいですが、果たして。

ただtailwind cssが推奨されているように、CSSフレームワークを導入してCSSのそのものを軽く済ますようにするのがよいのかもですね。

Layout Component

layoutファイルを作りましょうという話。これはとても使いやすくて良さそう。ただ、layoutという特殊な役割ではなく、componentの一種という感じであろうか。実際の運用上はcomponents配下にlayoutsとpartsとか作って分けた方が良さそう。

そしてここで唐突にきた新しい概念がCSS modules。どうやったら動作するのかはわかったけどちょっと慣れない。最近Twitterのフロントエンドの人たちの間で見たワードな気がするので、要チェックや。

なお、globalなCSSを読み込ませたい場合は、pages/_app.jsを作ってimportする。
ここでlayoutファイルと_app.jsの違いがとても気になった。

This App component is the top-level component which will be common across all the different pages. You can use this App component to keep state when navigating between pages, for example.

こちらを読むと、top-level componentという言い方をしているのでアプリ内の全てのcomponentを外からラップする感じになるのかな?よくよく考えたらrailsにwebpacker載せた時にApp.vueとか作ったりしたのでそのような物に近そう。
結局layoutは自作なので、全体の設定はappを使うべきなのだろう。
以下の記事は参考になりました。
https://qiita.com/tetsutaroendo/items/c7171286137d963cdecf

awakeiawakei

Next.jsのチュートリアル(その3)

(毎日亀の歩み…)

Assets, Metadata, and CSS

Polishing Layout

いきなりCSS modulesの実用感あるチュートリアル。
ひとまずコードを貼り付けて動作は確認したのでコードを読み解いていきます。

まず、styles/utlis.module.cssを作成します。これが実際のglobal cssの推奨ぽいですね。
CSS modulesはなるほどなあ、という感じであるのですが、ちょいと冗長な気がしなくも無い…
CSS modulesの概念をいまいち掴み切れていないのでここらへん要チェックですね。

https://qiita.com/Quramy/items/a5d8967cdbd1b8575130
https://postd.cc/css-modules/

awakeiawakei

Next.jsのチュートリアル(その4)

Pre-rendering and Data Fetching

pre-renderingの基本

とてもコアな部分ですね。

By default, Next.js pre-renders every page. This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering can result in better performance and SEO.

Next.jsはpre-renderを各ページに行う。つまり、クライアントJavaScriptの処理抜きに、前もってそれぞれのページのHTMLを準備しておきます。SEOにおいてはよりよい結果をもたらす。(拙訳)

Each generated HTML is associated with minimal JavaScript code necessary for that page. When a page is loaded by the browser, its JavaScript code runs and makes the page fully interactive. (This process is called hydration.)

生成されたHTMLはページに必要な最小限のJavaScriptとともに関連づけられる。ページがブラウザでロードされたとき、(そのページの)JavaScriptは実行され、ページを完全にインタラクティブにする(このプロセスをhydrationと呼ぶ)(拙訳)

もうなんかすごいなあ、という感想しかない。完全実用ベースの素晴らしい仕組みですね。
近年のwebの高速化対応って、結構自分でこねくり回したり金で殴ることが多い気がするのですが、(特にrailsだと)そこを標準的に搭載してあるのは助かります。

実際のデモとして、chormeのjavascript実行をdisableして、next.jsのアプリとreactのアプリにアクセスしてみると、前者はjsなしでも動きました。なるほど。

そしてpre-rederingにはStatic GenerationとServer-side Renderingがある。(よくみるやつ!)

  • Static Generation : buildのタイミング でHTMLがpre-renderされて、リクエストごとに使いまわされる
  • Server-side Rendering : 各リクエストのタイミング でHTMLがpre-renderされる

そして、next.jsはこの二つを同一アプリ内でハイブリッドに使えるそう。すごいな。
それぞれ用途に向き不向きがあるそうだけど、少なくとも今回のブログにおいてはStatic Generationがよさげですね。

Static Generation with Data

ここまでのチュートリアルではDataを使わずにページを生成してきたので、ここからはasync functionの getStaticPropsを使ってデータを必要とするページを作成していきます。

まず、トップレベルにpostsディレクトリを作成し、チュートリアル用のmdファイルを2つ作成する。
そのあとgray-matter というYAML Front Matter(mdファイルの冒頭につける、YAML形式のメタデータ)を解析するパーサーをいれて、lib/posts.js というファイルシステムから記事情報を取得するライブラリを作成します。

このライブラリは何をするのか。コードを見ながら理解していきます。

fs / path はファイルシステムのためのライブラリですね(多分)
それを用いて、postsディレクトリから.mdのファイルを取得し、mapで以下の処理を回します。

  • ファイル名から.mdを除いたものを idに入れる
  • ファイル内のmarkdownをutf8の文字列で fileContents に入れる
  • gray-matterを使って、fileContentsをparseする
  • idとparseしたデータを展開してreturn

こうしてmapして得た、 allPostDatadate ソートして返却する。

こんな感じでしょうか。

lib/index.js(引用)
import fs from "fs";
import path from "path";
import matter from "gray-matter";

const postsDirectory = path.join(process.cwd(), "posts");

export function getSortedPostsData() {
  // Get file names under /posts
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData = fileNames.map((fileName) => {
    // Remove ".md" from file name to get id
    const id = fileName.replace(/\.md$/, "");

    // Read markdown file as string
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, "utf8");

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents);

    // Combine the data with the id
    return {
      id,
      ...matterResult.data,
    };
  });
  // Sort posts by date
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1;
    } else {
      return -1;
    }
  });
}

そのあとこの getSortedDatapages/index.jsgetStaticPropsの中で呼び出して、propsに入れます。
propsにいれたら、Homeコンポーネントに渡してあげて、htmlに展開すると、表示されました!

今回はファイルシステムへのアクセスでしたが、これが仮にAPIだったら、lib/posts.js をapiラッパーとして使えばよいし、DBに直アクセスしても良いとのこと。(実際はこっちですね)

awakeiawakei

Next.jsのチュートリアル(その4)

Dynamic Routes

動的なルーティングを可能にしていきます。今回だと先ほどのidをそのまま/posts/${id}みたいjにする感じですね。
nuxtだとpages/post/_id.vueだけど、nextだとpages/post/[id].jsなんですね。
ただ、ここで新概念getStaticPath。これはidのリストを返却させるものだそう。pageコンポーネント内で細かいルーティングを作る感じだろうか。

このidのリストに関しては、lib/posts.jsに以下のような配列を返却させる getAllPostIds という関数を作成します。

getAllPostIdsの返り値(引用)
[
  {
    params: {
      id: 'ssg-ssr'
    }
  },
 {
    params: {
      id: 'pre-rendering'
    }
  }
]

なお、「単純なStringの配列ではダメなのか」という問いに対しては、

it must be an array of objects that look like the comment above. Each object must have the params key and contain an object with the id key (because we’re using [id] in the file name). Otherwise, getStaticPaths will fail.

オブジェクトの配列でないといけない。それぞれのオブジェクトはparamsのキーを含み、idのキーをもつオブジェクトを持たなければいけない。(idであるのは[id]というファイル名だから。)でなければ、getStaticPathsはうまく動かないだろう(拙訳)

だそうです。これはおそらくidという文字列を可変にするためなのかな?slugとか使う場合もあるだろうし。

ということで作成したgetAllPostIdsgetStaticPaths で呼び出させた。
そのあと、idをもとに個々のpostの内容を取得する関数を作成し、paramsの値を使ってgetStaticPropsで呼び出し、ひとまず簡易な動的ルーティングの表示を完成。

次はmarkdownの表示。
ここではremarkというライブラリを使う。
使っていくううう、と思ったらちょっと驚いたのが、lib/posts.jsで呼び出して使うということ。
まあ確かにSSGの恩恵を受けようと思ったらわざわざクライアントサイドで実行される側に書く必要ないですね。
ここらへん処理がどこで実行されるかで考えないといけない。
※remarkの呼び出しはawaitを使うので、getPostData()はasync関数にしないといけない。ここらへんは後日ちゃんと理解する。

最後markdownをパースしたコンテンツを表示する時に dangerouslySetInnerHTMLという禍々しいやつが出てきたのですが、vueでいうv-htmlみたいです。XSSの危険性もあるため禍々しくしてるみたいですね。
https://ja.reactjs.org/docs/dom-elements.html

awakeiawakei

Next.jsのチュートリアル(その4)

Dynamic Routes(Formatting the Date)

こちらはあと日時のformatとcssの読み込みなので割愛します。
リッチになりました。
こういうチュートリアルでも簡単にそれっぽくできるのはテンション上がっていいですよね。

あとfallbackに関しては、falseにするとマッチしない場合、404を返すようにできるのですね。
これは大事。

awakeiawakei

Next.jsのチュートリアル(その5)

(結構間があいてしまったが挫けず頑張る)

API Routes

which let you easily create an API endpoint as a Node.js serverless function

簡単にNode.jsのサーバーレスなapiエンドポイントが作れるよ(拙訳)

とのことです。
ここで不勉強ながら単なるフロントエンドのフレームワークだと思っていたので面食らいました。。
なるほど、たしかにSSRもあるくらいだからね…

要点としては、pages/api以下にhello.jsを作って、以下のような簡単な200とテキストを返すように設定する。
そして、http://localhost:3000/api/hello へアクセスすると、{"text": "Hello" }を返ってくる。

pages/api/hello.js(引用)
export default function handler(req, res) {
  res.status(200).json({ text: 'Hello' })
}

とても簡単。pages以下に置くと勝手にルーティングしてくれるnext.jsの便利さをそのまま使ってapiのルーティングができるんですね。

なお、

You should not fetch an API Route from getStaticProps or getStaticPaths. Instead, write your server-side code directly in getStaticProps or getStaticPaths (or call a helper function).

とのことで、getStaticPropsgetStaticPathsがサーバーサイドでしか動かないので、ブラウザのjsバンドルに含まれないためらしい…ちょっと理解し切れていないので追って調べる。

POSTとかで使うといいよーとのこと。