はじめに
仕事で Next.js を使っているので、開発環境を初期構築する方法をメモします。
TypeScript と Material-UI を含めた鉄板ネタ?で簡単に環境構築できます。
前提
パッケージマネージャーとして yarn をインストールしています。
Next.js
React をベースにしたフレームワークで、SSRやらSSGが簡単に実現できます。
その中でも私はファイルベースのルーティングが好きなので、SSRやらSSGが必要なくても Next.js を選択したいものです。
Next.js の詳細はこちらの公式ページを参照。
TypeScript
言わずもがな JavaScript に静的な型付けができます。
Material-UI
私のようなデザインセンスのないエンジニアでも、簡単にカッコいいWebデザインを作れる(かもしれない)UIコンポーネントを提供してくれます。
Material-UI の詳細はこちらの公式ページを参照。
Next.js のプロジェクトを構築する
まず Next.js のプロジェクトを構築していきます。
React の create-react-app
と同じように create-next-app
で簡単に初期構築できます。
また、サンプルがたくさん用意されているので、自分の目的に合わせて近しいプロジェクトを選択して初期構築することができます。
例えば今回だと Material-UI のプロジェクトを作ってみようと思います。
はい。ありましたね。
サンプルから初期構築する場合は -e
オプションを使います。
npx create-next-app -e with-material-ui my-material-ui
てな感じです。簡単ですね。
ではやってみます。
$ npx create-next-app --example with-material-ui my-material-ui
Could not locate an example named "with-material-ui". Please check your spelling and try again.
失敗しました!
サンプルは既にこちらに移動されているようです。なるほど。
仕方ないので別のサンプルを使ってみます。
$ npx create-next-app -e with-typescript-eslint-jest my-nextjs
Creating a new Next.js app in /../my-nextjs.
Downloading files for example with-typescript-eslint-jest. This might take a moment.
Installing packages. This might take a couple of minutes.
added 1099 packages, and audited 1100 packages in 36s
106 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Initialized a git repository.
Success! Created my-nextjs at /../my-nextjs
Inside that directory, you can run several commands:
npm run dev
Starts the development server.
npm run build
Builds the app for production.
npm start
Runs the built app in production mode.
We suggest that you begin by typing:
cd my-nextjs
npm run dev
今度は成功しました!
yarn run dev
後に localhost:3000
にアクセスして以下のページが表示されました。
Material-UI を追加する
ここにある通り、手動で追加することにします。
$ yarn add @material-ui/core
yarn add v1.22.10
info No lockfile found.
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 223 new dependencies.
・・・
pages/index.tsx
を以下のようにして Material-UI を適用してみます。
import Head from 'next/head'
import Image from 'next/image'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import Container from '@material-ui/core/Container'
import { makeStyles } from '@material-ui/core/styles'
import CssBaseline from '@material-ui/core/CssBaseline'
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
},
main: {
marginTop: theme.spacing(8),
marginBottom: theme.spacing(2),
},
footer: {
padding: theme.spacing(3, 2),
marginTop: 'auto',
backgroundColor: theme.palette.primary.main,
color: 'white',
textAlign: 'center',
},
}))
export const Home = (): JSX.Element => {
const classes = useStyles()
return (
<div className={classes.root}>
<CssBaseline />
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<header>
<AppBar position="relative">
<Toolbar>
<Typography variant="h6" color="inherit" noWrap>
{"Material-UI"}
</Typography>
</Toolbar>
</AppBar>
</header>
<main className={classes.root}>
<Container component="main" className={classes.main} maxWidth="md">
<h1 className="title">
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className="description">
Get started by editing <code>pages/index.tsx</code>
</p>
<button
onClick={() => {
window.alert('With typescript and Jest')
}}
>
Test Button
</button>
<div className="grid">
<a href="https://nextjs.org/docs" className="card">
<h3>Documentation →</h3>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className="card">
<h3>Learn →</h3>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/vercel/next.js/tree/master/examples"
className="card"
>
<h3>Examples →</h3>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className="card"
>
<h3>Deploy →</h3>
<p>Instantly deploy your Next.js site to a public URL with Vercel.</p>
</a>
</div>
</Container>
</main>
<footer className={classes.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<Image src="/vercel.svg" alt="Vercel Logo" height={'32'} width={'64'} />
</a>
</footer>
<style jsx>{`
// 変更なしのため割愛
・・・
`}</style>
</div>
)
}
export default Home
- 補足
-
<style>
の中身は初期状態から変更なし。長いので省略。 - 長くなってしまったので、本来であればコンポーネントを分割すべきでしょうね。
-
画面はこんな感じです。
ただ、コードを修正してページ更新してみるとブラウザのコンソールにこんなエラーが。。。
Warning: Prop `className` did not match. Server: "MuiContainer-root makeStyles-main-5 MuiContainer-maxWidthMd" Client: "MuiContainer-root makeStyles-main-2 MuiContainer-maxWidthMd"
at main
at Container (webpack-internal:///./node_modules/@material-ui/core/esm/Container/Container.js:85:23)
at WithStyles (webpack-internal:///./node_modules/@material-ui/styles/esm/withStyles/withStyles.js:61:31)
at main
at div
at Home (webpack-internal:///./pages/index.tsx:58:17)
at App (webpack-internal:///./node_modules/next/dist/pages/_app.js:77:5)
at ErrorBoundary (webpack-internal:///./node_modules/@next/react-dev-overlay/lib/internal/ErrorBoundary.js:23:47)
at ReactDevOverlay (webpack-internal:///./node_modules/@next/react-dev-overlay/lib/internal/ReactDevOverlay.js:73:23)
at Container (webpack-internal:///./node_modules/next/dist/client/index.js:155:5)
at AppContainer (webpack-internal:///./node_modules/next/dist/client/index.js:643:24)
at Root (webpack-internal:///./node_modules/next/dist/client/index.js:779:25)
そこで先程紹介した移動したサンプルを参考に、以下のファイルを追加します。
- 追加したファイル
- pages/_app.tsx
- pages/_document.tsx
import React from 'react'
import Head from 'next/head'
import { AppProps } from 'next/app'
import CssBaseline from '@material-ui/core/CssBaseline'
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
React.useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side')
if (jssStyles && jssStyles.parentElement) {
jssStyles.parentElement.removeChild(jssStyles)
}
}, [])
return (
<React.Fragment>
<CssBaseline />
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Component {...pageProps} />
</React.Fragment>
)
}
export default App
- _app.tsx の補足
- 全ページ共通の処理を実装できますが、今回のエラーとは特に関係なさそう。
- だけどあると便利なので、pages/index.tsx で実装した
<CssBaseLine />
と<Head>
はこちらに移動してしまう。
import { ServerStyleSheets } from '@material-ui/core/styles'
import Document, { Head, Html, Main, NextScript } from 'next/document'
import React from 'react'
export default class MyDocument extends Document {
render(): JSX.Element {
return (
<Html lang="ja-JP">
<Head>
{/* PWA primary color */}
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
// Render app and page and get the context of the page with collected side effects.
const sheets = new ServerStyleSheets()
const originalRenderPage = ctx.renderPage
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
}
}
- _document.tsx の補足
- エラー解消にはこちらのファイルの
MyDocument.getInitialProps
の定義のところが重要です。 - 余談ですがクラスコンポーネントになってますね。こちらのファイルはサンプルをほぼ丸コピしてます。
- エラー解消にはこちらのファイルの
これでコンソールにエラーは出なくなりました!
静的HTMLを出力する
Next.js には next export
というコマンドが用意されており、簡単に静的なHTMLを出力できます。
そこで package.json
を以下のように修正して、ビルド時に静的HTMLを出力するようにします。
diff --git a/package.json b/package.json
index 64f8a47..d2ecd0d 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"version": "1.0.0",
"scripts": {
"dev": "next dev",
- "build": "next build",
+ "build": "next build && next export",
"start": "next start",
"type-check": "tsc --pretty --noEmit",
"format": "prettier --write .",
@@ -26,6 +26,7 @@
]
},
"dependencies": {
"@material-ui/core": "^4.11.4",
"next": "latest",
"react": "^17.0.1",
"react-dom": "^17.0.1"
yarn run build
を実行します。
$ yarn run build
yarn run v1.22.10
$ next build && next export
info - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5
info - Checking validity of types
info - Using external babel configuration from /../my-nextjs/.babelrc
info - Creating an optimized production build
info - Compiled successfully
info - Collecting page data
info - Generating static pages (3/3)
info - Finalizing page optimization
Page Size First Load JS
┌ ○ / 10.3 kB 97.2 kB
├ /_app 0 B 86.9 kB
├ ○ /404 3.71 kB 90.6 kB
└ λ /api/hello 0 B 86.9 kB
+ First Load JS shared by all 86.9 kB
├ chunks/205.fc6391.js 7.94 kB
├ chunks/433.83deda.js 12.6 kB
├ chunks/894.a1bad4.js 22 kB
├ chunks/framework.aa6452.js 42.4 kB
├ chunks/main.5aaba5.js 154 B
├ chunks/pages/_app.f0c4dc.js 801 B
└ chunks/webpack.231c1c.js 993 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)
(ISR) incremental static regeneration (uses revalidate in getStaticProps)
info - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5
info - using build directory: /../my-nextjs/.next
info - Copying "static build" directory
info - No "exportPathMap" found in "next.config.js". Generating map from "./pages"
Error: Image Optimization using Next.js' default loader is not compatible with `next export`.
Possible solutions:
- Use `next start` to run a server, which includes the Image Optimization API.
- Use any provider which supports Image Optimization (like Vercel).
- Configure a third-party loader in `next.config.js`.
- Use the `loader` prop for `next/image`.
Read more: https://nextjs.org/docs/messages/export-image-api
at /../my-nextjs/node_modules/next/dist/export/index.js:14:785
at async Span.traceAsyncFn (/../my-nextjs/node_modules/next/dist/telemetry/trace/trace.js:6:584)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
はい。失敗します!
どうやら原因はここにあるようです。
import Image from 'next/image'
・・・
<Image src="/vercel.svg" alt="Vercel Logo" height={'32'} width={'64'} />
こんなIssueがありますね。
どうやらnext/image
は、画像の表示をサーバー側で最適化してくれるコンポーネントっぽいです。
今回は特に不要なので React の <img>
に置き換えました。
<img src="/vercel.svg" alt="Vercel Logo" height={'32'} width={'64'} />
再び yarn run build
を実行します。
$ yarn run build
yarn run v1.22.10
$ next build && next export
info - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5
info - Checking validity of types
info - Using external babel configuration from /../my-nextjs/.babelrc
info - Creating an optimized production build
info - Compiled successfully
info - Collecting page data
info - Generating static pages (3/3)
info - Finalizing page optimization
Page Size First Load JS
┌ ○ / 7.35 kB 93.8 kB
├ /_app 0 B 86.5 kB
├ ○ /404 3.71 kB 90.2 kB
└ λ /api/hello 0 B 86.5 kB
+ First Load JS shared by all 86.5 kB
├ chunks/262.b5f9f9.js 21.7 kB
├ chunks/284.ad3787.js 7.54 kB
├ chunks/433.d1602d.js 12.9 kB
├ chunks/framework.aa6452.js 42.4 kB
├ chunks/main.b89501.js 154 B
├ chunks/pages/_app.c8ce47.js 802 B
└ chunks/webpack.1bcf44.js 993 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)
(ISR) incremental static regeneration (uses revalidate in getStaticProps)
info - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5
info - using build directory: /../my-nextjs/.next
info - Copying "static build" directory
info - No "exportPathMap" found in "next.config.js". Generating map from "./pages"
info - Launching 3 workers
warn - Statically exporting a Next.js application via `next export` disables API routes.
This command is meant for static-only hosts, and is not necessary to make your application static.
Pages in your application without server-side data dependencies will be automatically statically exported by `next build`, including pages powered by `getStaticProps`.
Learn more: https://nextjs.org/docs/messages/api-routes-static-export
info - Copying "public" directory
info - Exporting (2/2)
Export successful. Files written to /../my-nextjs/out
✨ Done in 15.24s.
今度は成功しました!
念の為動作を確認します。
$ cd out
$ npx http-server
Starting up http-server, serving ./
Available on:
http://127.0.0.1:8080
http://192.168.100.49:8080
Hit CTRL-C to stop the server
localhost:8080
にアクセスして以下のページが表示されました。
あとは S3 やら Firebase Hosting やら適当にデプロイしてくだされ。
まとめ
- Next.js で Material-UI を使いたい場合は
create-next-app
ではなく、このページのサンプルをクローンした方が早そう。 -
create-next-app
で初期構築したプロジェクトに後からMaterial-UI
を入れる場合はこのページのサンプルから_app.tsx
、_document.tsx
を忘れずにパクる。 - 静的HTMLを出力する場合は
next/image
コンポーネントのご利用は慎重に。
ちなみに
pages
ディレクトリは src
配下に移動してもルーティングとして認識してくれます。
私は src
配下に pages
がある方が好きだな。