Nextjs×Typescriptで始めるJAMstackブログ
はじめに
やりたいこと
- 自分自身のブログページ(というより個人ページ?)を作りたい
- react使ってみたいのでNextjsで
- Typescriptも使ってみる
- JAMstack
- 動かす先はvercel
プロジェクト作成と初期設定(その1)
create-next-app
まずは、プロジェクトを作成する。
create-next-app
とwith-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のスクリプトを実行しやすいように登録しておきます。
{
"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の設定
各種設定ファイルは下記を参考にしたので直接ご覧ください。
(ちゃんと完全に理解して書けるようになりたい…)
一度実行
$ yarn lint
実行したのですがたくさんエラーでてきた。。。
ひとまずeslintが動いたので次から直していきたいと思います。
参考
プロジェクト作成と初期設定(その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
こちらですが、以下のようなファイルで出ています。
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にしてもよさそう。
"rules": {
"react/prop-types": "off",
"react/react-in-jsx-scope": "off"
},
(もっと良い対応方法があれば知りたいです)
Missing return type on function
直訳すると関数の返り値にも型をつけなさい、とのこと。(そりゃそうだ)
warningだけど直していきたいと思います。
初めは以下のような感じですが、初期でwarningがでているのはcomponentのreturnのとこなので、ReactElement
を読み込んで、ReactElement
と書いていきます。
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そのものも理解しながら進めていきたいと思います。
Next.jsのチュートリアル
「あれ、そういえばnextのことなんにもわからなくね?」となったので一旦脱線して、Next.jsのチュートリアルをやります。
こちらを使います。
公式だし、ステップごとに分けられておりわかりやすいので普通にこれでいいと思います。
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から。
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を使うべきなのだろう。
以下の記事は参考になりました。
Next.jsのチュートリアル(その3)
(毎日亀の歩み…)
Assets, Metadata, and CSS
Polishing Layout
いきなりCSS modulesの実用感あるチュートリアル。
ひとまずコードを貼り付けて動作は確認したのでコードを読み解いていきます。
まず、styles/utlis.module.css
を作成します。これが実際のglobal cssの推奨ぽいですね。
CSS modulesはなるほどなあ、という感じであるのですが、ちょいと冗長な気がしなくも無い…
CSS modulesの概念をいまいち掴み切れていないのでここらへん要チェックですね。
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して得た、 allPostData
を date
ソートして返却する。
こんな感じでしょうか。
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;
}
});
}
そのあとこの getSortedData
を pages/index.js
のgetStaticProps
の中で呼び出して、propsに入れます。
propsにいれたら、Homeコンポーネントに渡してあげて、htmlに展開すると、表示されました!
今回はファイルシステムへのアクセスでしたが、これが仮にAPIだったら、lib/posts.js
をapiラッパーとして使えばよいし、DBに直アクセスしても良いとのこと。(実際はこっちですね)
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
という関数を作成します。
[
{
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とか使う場合もあるだろうし。
ということで作成したgetAllPostIds
をgetStaticPaths
で呼び出させた。
そのあと、idをもとに個々のpostの内容を取得する関数を作成し、paramsの値を使ってgetStaticProps
で呼び出し、ひとまず簡易な動的ルーティングの表示を完成。
次はmarkdownの表示。
ここではremark
というライブラリを使う。
使っていくううう、と思ったらちょっと驚いたのが、lib/posts.js
で呼び出して使うということ。
まあ確かにSSGの恩恵を受けようと思ったらわざわざクライアントサイドで実行される側に書く必要ないですね。
ここらへん処理がどこで実行されるかで考えないといけない。
※remarkの呼び出しはawaitを使うので、getPostData()
はasync関数にしないといけない。ここらへんは後日ちゃんと理解する。
最後markdownをパースしたコンテンツを表示する時に dangerouslySetInnerHTML
という禍々しいやつが出てきたのですが、vueでいうv-html
みたいです。XSSの危険性もあるため禍々しくしてるみたいですね。
Next.jsのチュートリアル(その4)
Dynamic Routes(Formatting the Date)
こちらはあと日時のformatとcssの読み込みなので割愛します。
リッチになりました。
こういうチュートリアルでも簡単にそれっぽくできるのはテンション上がっていいですよね。
あとfallbackに関しては、falseにするとマッチしない場合、404を返すようにできるのですね。
これは大事。
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" }
を返ってくる。
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).
とのことで、getStaticProps
とgetStaticPaths
がサーバーサイドでしか動かないので、ブラウザのjsバンドルに含まれないためらしい…ちょっと理解し切れていないので追って調べる。
POSTとかで使うといいよーとのこと。