React+Djangoでブログを作る4〈トップページ作成編〉

今回は、Reactでページの表示部分を作っていきます。

モックアップを作る

まず初めに、ページのモックアップを作ります。

モックアップとは、工業製品の設計・デザイン段階で試作される、外見を実物そっくりに似せて作られた実物大の模型のこと。ソフトウェアやWebサイト、印刷物などのデザインを確認するための試作品のこともこのように呼ばれることがある。

――― IT用語辞典 e-Words

作ろうと思いましたが、この手のモックアップツールは大半が有料みたいなのでペイントで済ませます。

出来ました。シンプルな2カラム右サイドバーのトップページです。

少々荒は目立ちますが、こんな感じで作るページのイメージを画像にしておきます。

ヘッダーは左側にブログのヘッダー画像かブログアイコンとブログタイトルが表示され、右側にドロップダウンメニューやリンクが表示されます。

それぞれの記事はアイキャッチ画像の上にタイトルと概要が表示されます。

サイドバーはまだ何を置くか決めていませんが、ウィジェット形式の何かが作成出来ると良いと思います。

Atomic Design とは

ウェブページを構成する要素を粒度を基準に分類する事で、要素の再利用性の向上や冗長性の削減を目指すという思想らしいです。

要素ベースでページを作るReact等の最近のフロントエンドフレームワークでウェブアプリケーションを作成する場合に役に立ちます。

分類は5つ有り、それぞれに以下のような名前と基準が付いています。

  • Atoms (原子) : それ以上分割出来ない要素
  • Molecules (分子) : 2つ以上の原子から構成される要素
  • Organisms (有機体) : いくつかの分子の集合
  • Templates (テンプレート) : データを流し込むための枠組み
  • Pages (ページ) : 実際のデータを含むテンプレート

実際にどのような要素が分類されるのかをメモしておきます。

分類 要素
Atoms Button, Icon, Text, Title
Molecules Card, Box, Form, Popup
Organisms Header, Calender, Modal, CardList
Templates TopPage, SinglePost, CategoryList, RegistrationPage
Pages 自己紹介ページとか

入力フォームは入力のためテキストボックスや送信のためボタン等のAtomの集合なのでMolecule、ヘッダーは様々なアイコン付きボタンや見出しテキストドロップダウンメニューから成り立っているのでOrganismsという感じのようです。

少々難しいですが、ウェブ開発を効率化するための思想に捕らわれて効率が落ちるのは本末転倒なので、あまり深くは考えずこの思想に緩く制約されようと思います。

ちなみに、Atomic Designに階層構造を取り入れたLayered Atomic Designという物もあるようです。

Reactでフロントエンドを作る

では、実際に作っていきます。

Material-UIのインストール

作り始める前に、React用のUIフレームワークの1つであるMaterial-UIをインストールします。

$ cd frontend
$ npm install @material-ui/core @material-ui/icons

コマンドを使って暫く待つとインストール完了です。

Atomic Designのためのディレクトリ構造を作る

前回は、ページを構成する要素を全てfrontend/src/App.jsに記述しました。

Atomic Designでは要素に対し分類がされているので、まずはそれぞれのディレクトリを作成します。

まずは要素を保存するための親ディレクトリをfrontend/src/components/に作成します。

そしてその中に、atomsmoleculesorganismstemplatespagesのディレクトリをそれぞれ作ります。

具体的には、以下のようなディレクトリ構造になります。

blogress/
|-- frontend
   |-- src/
      |-- components/
         |-- atoms/ (原子)
         |-- molecules/ (分子)
         |-- organisms/ (有機体)
         |-- templates/ (テンプレート)
         |-- pages/ (ページ)
   |-- ...
|-- ...

これらのディレクトリに適した物を書く事で、Reactのコンポーネント志向なウェブアプリケーション作成が自然と出来ます。

定数ファイルを作る

定数用のファイルを作る設計が良いのかは置いておいて、定数を置いておくためにfrontend/src/components/constants.jsxを作成してブログ名等を記述しておきます。

export const BLOG_TITLE = 'My Blogress Life';
export const BLOG_DESCRIPTION = 'This is my blog made in simple blog system named Blogress.'

将来的にはこれをAPIから読み込む形にするので、このように一ヶ所から参照する形にするようにしておくと恐らく便利です。

ヘッダーを作る

何処から作っても良いのですが、まずはヘッダーから作る事にします。

ReactはReactコンポーネントと呼ばれる要素単位を併せてページを作成するので、ヘッダーのReactコンポーネントを作成します。

frontend/src/components/organisms/Header.jsxに以下のように記述してください。

import React, { Fragment } from 'react';
import { makeStyles } from '@material-ui/core/styles';

import { AppBar, Toolbar, Typography } from '@material-ui/core';
import SportsVolleyballIcon from '@material-ui/icons/SportsVolleyball';

import { BLOG_TITLE } from '../constants';

const useStyles = makeStyles(theme => ({
    offset: theme.mixins.toolbar,
}))

const Header = () => {
    const classes = useStyles();
    return(
      <Fragment>
        <AppBar position='fixed'>
            <Toolbar>
                <SportsVolleyballIcon />
                <Typography variant="h6" color="inherit" noWrap>
                    {BLOG_TITLE}
                </Typography>
            </Toolbar>
        </AppBar>
        <div className={classes.offset}/>
      </Fragment>
    );
}

export default Header;

今の所は特に何の機能も無い、ブログタイトルと謎のアイコンだけが存在している簡易なヘッダーです。

Atomic Designでファイル分割を行う

ヘッダーが完成したので早速トップページに設置して確認したい所ですが、ヘッダーのReactコンポーネントを利用する前に、frontend/src/App.jsの中身をAtomic Designに従って分割します。

オレオレAtomic Designかも知れませんが、frontend/src/App.jsを以下のように分割します。

frontend/src/components/templates/TopPageTemplate.jsxでは、トップページの骨組みを作成します。

import React, { useState, Fragment } from 'react';

import Header from '../organisms/Header';

const TopPageTemplate = props => (
    <Fragment>
        <Header />
        {props.posts.map(post => (
            <div key={post.id}>
            <h1>{post.title}</h1>
            <p>{post.body}</p>
            </div>
        ))}
    </Fragment>
);

export default TopPageTemplate;

このテンプレートに対して、frontend/src/components/pages/TopPage.jsxがブログの記事データをpropsで流し込むような構造にします。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

import { makeStyles } from '@material-ui/core/styles';

import TopPageTemplate from '../templates/TopPageTemplate';

const useStyles = makeStyles(theme => ({
    page: {
        margin : 60,
    }
}));

const TopPage = () => {
    const classes = useStyles();
    const [posts, setPosts] = useState([]);

    useEffect(() => {
        axios
            .get('http://localhost:8000/api/posts/')
            .then(res=>{setPosts(res.data);})
            .catch(err=>{console.log(err);});
    }, []);

    return(
        <TopPageTemplate className={classes.page} posts={posts} />
    );
}

export default TopPage;

従って、frontend/src/App.jsでは以下のようにfrontend/src/components/pages/TopPage.jsxの表示のみを行います。

import React from 'react';

import TopPage from './components/pages/TopPage';

const App = () => {
  return(
    <TopPage />
  );
}

export default App;

多少オレオレAtomic Designな気がしますが、これが正しいと言い聞かせながら進みます。

では、http://localhost:3000/にアクセスして確認しましょう。

きちんとヘッダーが付いているのが確認出来ました。

記事タイルグリッドを作る

そのまま記事の一覧表示も作ります。

私のイメージを図にしたモックアップの通り、今回は記事タイルがグリッド表示されている物を作ります。

まずは、Atomic Designに従って、frontend/src/components/PostTile.jsxに単体の記事タイルを作ります。

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';

import { Card, CardActionArea, CardContent, CardMedia, Typography } from '@material-ui/core';

const useStyles = makeStyles({
  card: {
    position: 'relative',
    maxWidth: 345,
  },
  media: {
    height: 345
  },
  content: {
    position: 'absolute',
    bottom: 0,
    width: '100%',
    color: 'white',
    backgroundColor: 'rgba(0,0,0,0.6)',
  },
  excerpt: {
      marginRight: 30,
  }
});

const PostTile = props => {
    const classes = useStyles();
    return (
        <Card className={classes.card}>
          <CardActionArea>
            <CardMedia
              className={classes.media}
              image={props.thumbnail}
              title={props.title}
            />
            <CardContent className={classes.content}>
              <Typography gutterBottom variant="h5" component="h2">
                {props.title}
              </Typography>
              <Typography variant="body2" component="p" className={classes.excerpt} noWrap>
                {props.body}
              </Typography>
            </CardContent>
          </CardActionArea>
        </Card>
      );
}

export default PostTile;

そして、記事タイルグリッドはこの記事タイルを複数用いた物なので、frontend/src/components/organisms/PostTileGrid.jsxに作成します。

import React from 'react';

import { Grid } from '@material-ui/core'

import PostTile from '../molecules/PostTile';

const PostTileGrid = props => (
    <Grid container spacing={4}>
        {props.posts.map(post => (
            <Grid item xs={4}>
                <PostTile title={post.title} body={post.body} thumbnail={post.thumbnail}/>
            </Grid>
        ))}
    </Grid>
);

export default PostTileGrid;

PostTileGridが完成したので、frontend/src/components/templates/TopPageTemplate.jsxで利用するように書き換えます。

import React, { Fragment } from 'react';
import { makeStyles } from '@material-ui/core/styles';

import Header from '../organisms/Header';
import PostTileGrid from '../organisms/PostTileGrid';

const useStyles = makeStyles(theme => ({
    postTileGrid: {
        margin: theme.spacing(4),
    }
}));

const TopPageTemplate = props => {
    const classes = useStyles();
    return (
        <Fragment>
            <Header />
            <div className={classes.postTileGrid}>
                <PostTileGrid posts={props.posts}/>
            </div>
        </Fragment>
    );
}

export default TopPageTemplate;

また、このままでは左右の余白がギリギリまで詰められてしまうので、frontend/src/index.cssを編集して、bodymin-width: 960px;を追加します。

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-width: 960px;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

これで、記事がタイル状でグリッド表示されるようになったはずです。

サムネイル表示にも対応しているため、Djangoの管理画面からpoststhumbnailを設定しながら、今は3つしかないサンプル記事をもう少し増やしてから確認します。

これまで見て来た質素な記事リストから、リッチな記事タイルグリッドになったのが確認出来たと思います。

また、frontend/src/index.cssでページの最低幅を指定しているので、横幅が小さくなりすぎても表示が崩れないようになっています。

フッターを作る

最後にフッターを作りましょう。

frontend/src/components/organisms/Footer.jsxを作成してください。

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';

import { Typography } from '@material-ui/core';

import { BLOG_TITLE } from '../constants';

function Copyright() {
    return (
        <Typography variant="body2" color="textSecondary" align="center">
            {'Copyright © '}{BLOG_TITLE}{' '}{new Date().getFullYear()}{'.'}
        </Typography>
    );
}

const useStyles = makeStyles(theme => ({
    footer: {
        backgroundColor: theme.palette.background.paper,
        padding: theme.spacing(6),
    },
}))

const Footer = () => {
    const classes = useStyles();
    return(
        <footer className={classes.footer}>
            <Copyright />
        </footer>
    );
}

export default Footer;

フッターのReactコンポーネントが完成したので、frontend/src/components/templates/TopPageTemplate.jsxで利用しましょう。

import React, { Fragment } from 'react';
import { makeStyles } from '@material-ui/core/styles';

import Header from '../organisms/Header';
import Footer from '../organisms/Footer';
import PostTileGrid from '../organisms/PostTileGrid';

const useStyles = makeStyles(theme => ({
    postTileGrid: {
        margin: theme.spacing(4),
    }
}));

const TopPageTemplate = props => {
    const classes = useStyles();
    return (
        <Fragment>
            <Header />
            <div className={classes.postTileGrid}>
                <PostTileGrid posts={props.posts}/>
            </div>
            <Footer />
        </Fragment>
    );
}

export default TopPageTemplate;

これで、記事タイルグリッドの後に今の所コピーライトの表示だけしてくれるフッターが表示されるはずです。

記事が作成日時で昇順になっていたりと気になる点は有りますが、完成です。

まとめ

今回で一応なブログのトップページのような形になりました。

成果が目で確認しやすいフロントエンドはやっていて楽しいですね。

参考

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です