セブではたらく(インターン)
セブではたらく(インターン)
2019.11.12

Next.jsを勉強してみる その② 〜画面作成編〜

やなさん

まいどです。

右腕が折れても、意外と仕事ってできるもんですね。3ヶ月前に右腕の上腕骨を骨折したバックエンドエンジニアのやなさんです。

さて、前回はNext.jsの開発環境を構築する記事を書きました。

今回はこの記事で構築した環境を使って、画面の遷移外部からデータを取得して画面に表示する部分を手探りのままに作ってみたいと思います。

では、さっそく。

※ このブログは、わからないなりにとりあえず動かすことを目標にしております。本来Reactで推奨する使い方と異なる部分があるかもしれませんが、それはご愛嬌ということで大目にみてください。

画面作成に必要なものをインストール

今回はTypeScriptで書いてみようと思うので、ターミナルで以下コマンドを実行します。

# typescriptで記述するので必要なライブラリをインストール
# Next.jsのプロジェクトルートで実行すること
npm install --save-dev typescript @types/react @types/react-dom @types/node

コンポーネントで画面のひな形を作成

各画面を作成する前にひな形となるファイルを作成します。

今回の雛形にはheadタグやシステム共通となるヘッダー、フッターを定義しています。

※ 画面のstyleは学習の論点ではなく、極力記述したくなかったため、CSSフレームワークのbulmaを読み込んでいます。

Componentsフォルダにlayout.tsxファイルを作成

// /src/app/components/layout.tsx
import * as React from 'react'
import Head from 'next/head'

type Props = {
  title?: string
  isHeader?: boolean
  isFooter?: boolean
}

function getHeader(title: string) {
  return (
    <header>
      <section className="hero is-dark">
        <div className="hero-body">
          <div className='container'>
            <h1 className="title">{title}</h1>
          </div>
        </div>
      </section>
    </header>
  );
}

function getFooter() {
  return (
    <footer className="footer">
      <div className="content has-text-centered">
        <p>Yana Test Screen</p>
      </div>
    </footer>
  );
}

const Layout: React.FunctionComponent<Props> = ({
  children,
  title = 'Yana Sample Screen Title',
  isHeader = true,
  isFooter = true,
}) => (
  <div>
    <Head>
      <title>{title}</title>
      <meta charSet="utf-8" />
      <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css"></link>
      <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
      <link rel="icon" type="image/x-icon" href="/static/favicon.ico" />
    </Head>
    {isHeader && (getHeader(title))}
    <section className="section">
      <div className="container">
        {children}
      </div>
    </section>
    {isFooter && (getFooter())}
  </div>
)

export default Layout

function getHeader()function getFooter()を関数にしている理由は、引数による分岐処理を書いてみたかったので入れています。

遷移前の画面を作成

遷移前の画面としてログインっぽい画面を作成します。

Pagesフォルダにlogin.tsxファイルを作成

// /src/app/pages/login.tsx
import * as React from 'react'
import Router from 'next/router'

import Layout from '../components/layout'

interface LoginProps {}
interface LoginState {
  credentials: {
    email: string
    password: string
  }
  isLoading: boolean
}

class Login extends React.Component<LoginProps, LoginState> {
  constructor(props: LoginProps) {
    super(props)
    this.state = {
      credentials: {
        email: null,
        password: null,
      },
      isLoading: false
    }
  }

  handleCredentialsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    let { credentials } = this.state
    credentials[e.target.name] = e.target.value

    this.setState({ credentials })
  }

  handleLoginSubmit = (e: React.MouseEvent<HTMLElement>) => {
    e.preventDefault()
    this.setState({ isLoading: true })

    setTimeout(() => {
      this.setState({ isLoading: false })
      // ボタンを押された場合に /user へ画面遷移
      Router.push('/user')
      // Router.replace('/user')
    }, 1000)
  }

  public render() {

    return (
      <Layout title="Login" isHeader={false} isFooter={false}>
        <form action="">
          <div className="login-area">
            <div>
              <h1 className=""><img src="/static/yanatch_black.png" alt="my image" /></h1>
            </div>
            <article className="box is-rounded">
              <div className="field">
                <label className="label">Email</label>
                <p className="control has-icons-left">
                  <input className="input" type="email" placeholder="Email" />
                  <span className="icon is-small is-left">
                    <i className="fas fa-envelope"></i>
                  </span>
                </p>
              </div>
              <div className="field">
                <label className="label">Password</label>
                <p className="control has-icons-left">
                  <input className="input" type="password" placeholder="Password" />
                  <span className="icon is-small is-left">
                    <i className="fa fa-lock"></i>
                  </span>
                </p>
              </div>
              <div className="field">
                <p className="">
                  <button className="button is-medium is-info is-fullwidth" onClick={this.handleLoginSubmit}>ログイン</button>
                </p>
              </div>
            </article>
          </div>
        </form>
        <style jsx>
        {`
          .login-area {
            margin: 0 auto;
            min-width: 375px;
            max-width: 400px
          }
          .box {
            padding-top: 3rem;
          }
          h1 {
            padding: 3rem 6rem;
            text-align: center;
          }

          .field {
            padding-bottom: 1.5rem;
          }
        `}
        </style>
      </Layout>
    );
  }

}

export default Login;

React.FunctionComponentとReact.Componentの使い分けはいろいろな意見があるようですが、基本的にはComponentのライフサイクルの利用有無で決めるようです。

ライフサイクルでどのような関数が実行されるかは、Reactのマニュアルのstate とライフサイクルを参照ください。

遷移後の画面を作成

遷移後の画面としてマイページっぽい画面を作成します。

APIを実行するために必要なライブラリをインストールします。

# isomorphic-unfetchをインストール
npm install --save isomorphic-unfetch

Pagesフォルダにuser.tsxファイルを作成

// /src/app/pages/user.tsx
import * as React from 'react'
import Layout from '../components/layout'
import fetch from "isomorphic-unfetch";

interface UserProps {
  data: {
    name: string
    enname: string
    birthday: string
    constellation: string
  }
}

class User extends React.Component<UserProps> {
  constructor(props: UserProps) {
    super(props)
  }
  static async getInitialProps(ctx: any) {
    try {
      let response;
      response = await fetch("http://localhost/api/user");
      const json = await response.json();
      return {
        data: json.data
      };
    } catch (e) {
      console.error(e);
      return {
        data: []
      };
    }
  }

  public render() {
    return (
      <Layout title="Yana Sample Test Screen">
        <div className="message is-info user-area">
          <h1 className="message-header">Introduction</h1>
          <div className="description">
            <figure className="image is-128x128">
              <img className="is-rounded" src="/static/yanagisawa.jpg" />
            </figure>
          </div>
    
          <p className="description name">{this.props.data.name}</p>
          <p className="description en-name">{this.props.data.enname}</p>
          <p className="description birthday">{this.props.data.birthday}</p>
          <p className="description constellation">{this.props.data.constellation}</p>
        </div>
        <style jsx>
          {`
            .user-area {
              margin: 0 auto;
              min-width: 375px;
              max-width: 600px;
              padding-bottom: 3rem;
              margin-bottom: 3rem;
            }
            .description {
              margin-top: .2rem;
              text-align: center;
            }
            .description figure {
              margin-top: 2rem;
              margin-bottom: 2rem;
              top: 0;
              left: 50%;
              -webkit-transform: translateX(-50%);
              transform: translateX(-50%);
            }
            .name {
              margin-top: 1rem;
              font-size: 1.4rem;
              font-weight: bold;
              line-height: 1.6rem;
            }
            .en-name {
              letter-spacing: -.05rem;
            }
            .birthday {
              font-size: .9rem;
              margin-top: .75rem;
            }
            .constellation {
              font-size: .9rem;
              margin-top: -.2rem;
            }
          `}
          </style>
      </Layout>
    );
  }
}

export default User

getInitialProps()の関数が画面表示時に実行され、http://localhost/api/userを呼び出します。

外部APIの呼び出し処理

上記で作成したファイルから呼び出している処理を作成します。

componentやpageの記述箇所によっては、サーバーかクライアントでどちらでも実行されることがあることと、SSR構成の場合、外部APIはクライアントから呼び出せないようにすることが一般的であることを考慮し、Next.jsのサーバ経由でAPIを呼び出すように作成します。

Next.js側に呼び出しAPIを作成

// /src/app/pages/api/user.ts
export default async(req, res) => {
  // 外部APIを呼び出し
  const response = await fetch("http://study-next_web_1:8081/user.php");
  const data = await response.json();
  res.status(200).json(data);
}

注意

study-next_web_1の部分はコンテナ名になっているので、動かしているコンテナ名に合わせて記載を変更してください。コンテナ名はdocker psを実行し、対象のコンテナ名を確認ください。

外部APIの処理を作成

呼び出して返却用jsonを記述するため、webフォルダにuser.phpを作成し、json返却用のphpを作成します。

<?php
// /src/web/user.php
$array = [
    'result' => 'OK',
    'data' => [
        'name' => '柳澤宏樹',
        'enname' => 'Hiroki Yanagisawa',
        'birthday' => '1982.09.20',
        'constellation' => '乙女座'
    ]
];
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json');
echo json_encode($array);

 
すべてを作成し、http://localhost/loginで以下の画面が表示され、以下のようにログインボタンを押すことでユーザページが表示されれば成功です!!

さいごに

今回は画面雛形のコンポーネント化、画面遷移、外部サーバからデータを取得する部分をなんとなく書いてみました。

説明が足りない部分もあるかと思いますが、Next.jsに興味がある方は、Next.jsのマニュアルとTypeScriptの情報を漁りながら、みなさんも一度試してみてください!

今回はComponent設計などぜんぜんできていない状態ですが、一歩ずつreactとNextjsの理解度が増してきている気がします。次はどの部分を学んでいくか検討中ですが、redux周りを勉強しようと思います!!

いままで作ったプログラムはgithubにも上がっておりますので、興味がある方は参照してみてください。

引き続き勉強します。

それではまた。やなさんでした。