BETA

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

投稿日:2020-03-29
最終更新:2020-04-10

ここ最近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-header  
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を実行するときにアクセス拒否されるのを防ぐために追記します。

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のルーティングを設定します。

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

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

バックエンドの実装

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

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  
from django.contrib import admin  
from .models import Todo  

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

class TodoSerializer(serializers.ModelSerializer):  
   class Meta:  
       model = Todo  
       fields = ('id', 'name', 'checked')  
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',)  
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を作成してください。

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  

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

API実装

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

src/common/apiディレクトリを作り、その中に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' });  
});  

コンポーネント実装

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

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/uichi/todo-frontend

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

参考

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

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@uichiの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう