Published on

まだMVCで消耗してるの?〜Django x Reactで始めるSPA開発〜

Authors

:::note 本記事の完全版チュートリアル 第二版が新しく出ました! https://note.com/nisyuu/n/nadc6f56b01de

最新バージョンに合わせて作り直しているほか、説明を新たに追加しているのでより進めやすくなっています! :::

ここ最近JSフレームワークを使ったサイトが増えてきています。 とくにReactやVueなどのJSフレームワークはSPAというアプリケーション開発によく使われ、サイトを利用するユーザーだけでなく開発者にも多くのメリットをもたらします。

想定読者

  • Web開発経験者
  • APIを使ったWebアプリケーションを開発したことがある人
  • JavaScriptをそこそこ知っててPythonもそこそこ知ってる人
  • Djangoをちょっと知っている
  • MVCもしくはMTVを使った開発をしたことがある人

別記事にもっと詳細に書いた記事があるので、本記事で難しいと感じた方やもっと深いところまで学習したい方はこちらをご覧ください。 まだMVCで消耗してるの?〜React x Djangoで始める今時Web開発〜

この記事ではフロントエンドにReact、バックエンドにDjangoを使用してチュートリアルを進めていきます。 チュートリアルはToDoアプリを題材にして進めていきます。

SPAとは

SPAはSingle Page Applicationと呼ばれ、ユーザーエクスペリエンスを向上させるのに有効な手立てとなります。 また、データバインディング、仮想DOM、Componentの3つの特徴を兼ね備えています。

データバインディング

素のJavaScriptを使って値を変更する場合、DOMを指定して値を変更する処理を毎回動かさなければなりません。 ですが、JSフレームワークを使うと定義しておいた変数が更新されるたびに画面上の値も変更されます。

仮想DOM

JSフレームワークには、クライアントのブラウザで描画をするためのDOMとサーバーとDOMの間に存在する仮想DOMの2種類があります。 仮想DOMの役割は、新しくサーバーから吐き出された仮想DOMと現在存在する仮想DOMとの差分を取り、その差分をDOMに反映することです。 そのためDOMの更新は差分があった部分だけとなり、ページのレンダリングを高速にすることができます。

Component

JSフレームワークでは、ページの要素をコンポーネントと呼ばれる部品単位に分割することができます。 そうすることで、コンポーネントを再利用することができ同じコードを書かずに済みます。

このチュートリアルではページを1枚作るだけなので、ユーザーエクスペリエンスにつながるメリットを肌で感じることはできないかもしれないのですが、開発面でのメリットは感じることができると思います。

Django環境構築

まずはバックエンドから進めていきます。

以下のコマンドを順に実行してください。

mkdir todo-backend
cd todo-backend
python3 -m venv env
source env/bin/activate
pip install django djangorestframework django-cors-headers
django-admin startproject project .
django-admin startapp todo
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

環境が構築できたら127.0.0.1:8000にアクセスしてください。 初期画面が表示されるはずです。

Django環境の設定

settings.pyにプラグイン追加の設定とクロスオリジンの設定を追記していきます。 クロスオリジンの設定は、WebブラウザからAPIを実行するときにアクセス拒否されるのを防ぐために追記します。

settings.py
INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'rest_framework',
   'corsheaders',
   'todo'
]

MIDDLEWARE = [
   'corsheaders.middleware.CorsMiddleware',
]

# 許可するオリジン
CORS_ORIGIN_WHITELIST = [
   'http://localhost:3000',
]

ついでにprojectディレクトリ内のurl設定ファイルに、APIのルーティングを設定します。

urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
   path('admin/', admin.site.urls),
   path('api/', include('todo.urls')),
]

バックエンドの実装

todoアプリ内を実装していきます。

models.py
from django.db import models

class Todo(models.Model):
   name = models.CharField(max_length=64, blank=False, null=False)
   checked = models.BooleanField(default=False)

   def __str__(self):
       return self.name

マイグレーションを実行します。

python manage.py makemigrations
python manage.py migrate
admin.py
from django.contrib import admin
from .models import Todo

@admin.register(Todo)
class Todo(admin.ModelAdmin):
   pass
serializer.py
from rest_framework import serializers
from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
   class Meta:
       model = Todo
       fields = ('id', 'name', 'checked')
views.py
from rest_framework import filters, generics, viewsets
from .models import Todo
from .serializer import TodoSerializer

class ToDoViewSet(viewsets.ModelViewSet):
   queryset = Todo.objects.all()
   serializer_class = TodoSerializer
   filter_fields = ('name',)
urls.py
from rest_framework import routers
from .views import ToDoViewSet
from django.urls import path, include

router = routers.DefaultRouter()
router.register(r'todo', ToDoViewSet)

urlpatterns = [
   path('', include(router.urls)),
]

ここまで終えたら、http://localhost:8000/admin にアクセスしてToDoをいくつか追加しておいてください。

React環境構築

Reactの環境立ち上げにはCreate React Appを使います。

yarn create react-app todo-frontend
cd todo-frontend
yarn start

http://localhost:3000にアクセスして画面が正常に表示されたら環境構築完了です。

ルーティング

Reactはルーティング機能を持たないので、別にプラグインをインストールします。

yarn add react-router-dom

srcディレクトリ直下にRouter.jsxを作成してください。

Router.jsx
import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import Top from '../components/Top';

const Router = () => {
 return (
   <BrowserRouter>
   </BrowserRouter>
 );
};
export default Router;

App.jsにルーティングを読み込ませます。

import React from 'react';
import Router from './configs/Router';

function App() {
 return (
   <Router />
 );
}

export default App;

画面デザイン

画面のデザインにはMaterial UIというデザインフレームワークを使います。 Reactのプラグインとして提供されているので、yarn addでインストールしてください。

yarn add @material-ui/core

下の画像が出来上がり図です。

Screenshot from 2020-02-16 00-50-08.png

API実装

まずはAPIを実装していきます。 一つのコンポーネント内に含めると可読性が落ちるので、別ファイルに分けてAPI処理を実装します。 実装するAPI処理は、ToDoリスト取得、ToDo作成、ToDoのチェック、ToDo削除の4つです。

src/common/apiディレクトリを作り、その中にtodo.jsを作成してください。

todo.js

const originUrl = 'http://127.0.0.1:8000';

const getTodoList = (() => {
  const url = new URL('/api/todo/', originUrl);
  return new Promise( (resolve, reject) => {
    fetch(url.href)
    .then( res => res.json() )
    .then( json => resolve(json) )
    .catch( () => reject([]) );
  });
});
export default getTodoList;

export const postCreateTodo = (name) => {
  const url = new URL('/api/todo/', originUrl);
  return new Promise( resolve => {
    fetch(url.href, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        name: name
      })
    })
    .then( res => res.json() )
    .then( data => resolve(data) );
  });
};

export const patchCheckTodo = ((id, check) => {
  const url = new URL(`/api/todo/${id}/`, originUrl);
  fetch(url.href, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      checked: check
    })
  });
});

export const deleteTodo = ((id) => {
  const url = new URL(`/api/todo/${id}/`, originUrl);
  fetch(url.href, { method: 'DELETE' });
});

コンポーネント実装

次にコンポーネントを実装します。

index.jsx
import React, { useEffect, useState } from 'react';
import Button from '@material-ui/core/Button';
import Box from '@material-ui/core/Box';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import Container from '@material-ui/core/Container';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import getToDoList, { postCreateTodo, patchCheckTodo, deleteTodo } from '../../common/api/todo';

const useStyles = makeStyles(theme => ({
  todoTextField: {
    marginRight: theme.spacing(1)
  }
}));

const Top = () => {
  const classes = useStyles();
  const [todoList, setTodoList] = useState([]);
  const [todo, setTodo] = useState('');

  useEffect(() => {
    (async () => {
      const list = await getToDoList();
      setTodoList(list);
    })();
  }, []);

  const handleCreate = async () => {
    if ( todo === '' || todoList.some( value => todo === value.name ) ) return;
    const createTodoResponse = await postCreateTodo(todo);
    setTodoList(todoList.concat(createTodoResponse));
  };

  const handleSetTodo = (e) => {
    setTodo(e.target.value);
  };

  const handleCheck = (e) => {
    const todoId = e.target.value;
    const checked = e.target.checked;
    const list = todoList.map( (value, index) => {
      if (value.id.toString() === todoId) {
        todoList[index].checked = checked;
      }
      return todoList[index];
    });
    setTodoList(list)
    patchCheckTodo(todoId, checked);
  }

  const handleDelete = (e) => {
    const todoId = e.currentTarget.dataset.id;
    const list = todoList.filter( value => value['id'].toString() !== todoId);
    setTodoList(list);
    deleteTodo(todoId);
  };

  return (
    <Container maxWidth="xs">
      <Box display="flex" justifyContent="space-between" mt={4} mb={4}>
        <TextField className={classes.todoTextField} label="やること" variant="outlined" size="small" onChange={handleSetTodo} />
        <Button variant="contained" color="primary" onClick={handleCreate}>作成</Button>
      </Box>
      <FormGroup>
        {todoList.map((todo, index) => {
          return (
            <Box key={index} display="flex" justifyContent="space-between" mb={1}>
              <FormControlLabel
                control={
                  <Checkbox
                    checked={todo.checked}
                    onChange={handleCheck}
                    value={todo.id}
                    color="primary"
                  />
                }
                label={todo.name}
              />
              <Button variant="contained" color="secondary" data-id={todo.id} onClick={handleDelete}>削除</Button>
            </Box>
          )
        })}
      </FormGroup>
    </Container>
  )
};
export default Top;

最後に

ToDoアプリを一つ作りましたが、この記事の内容だけだとまだ実用はできないので、いずれホスティングに載せるところまでを紹介しようと思います。

誤字脱字や、間違いがあればご連絡ください。 ソースコードをGitHubに上げているので、必要であれば使ってください。

フロントエンド https://github.com/nisyuu/todo-frontend

バックエンド https://github.com/nisyuu/todo-backend

参考

React公式 Material UI公式 Django公式 Django REST framework公式