古生代紀のブログ

主にアプリ開発/その他ナレッジ

フルスタック開発 Django + React プロジェクト作成手順 動画配信アプリ

この記事はこの方のプロジェクトの作成手順の備忘録です(個人用メモです)
https://github.com/GomaGoma676/YOUTUBE_API.git
https://github.com/GomaGoma676/Youtube_React


Backendの実装 Dango Rest API

1. 仮想環境作成

Anaconda Navigatorで
Environments>Createで新たな仮想環境作成

2. ターミナルを開き、必要なパッケージをインストールする

基本的には以下のパッケージをインストール

django - 言わずもがな
djangorestframework - Rest APIが使用できるようになる
djangorestframework-simplejwt - よりセキュリティレベルの高いTokenを使用できる
djoser - JWTと関連するパッケージ
pillow - 画像を扱うことができる
django-cors-headers - バックエンドとフロントエンドをクロスエンジンでつなぐ

3. PyCharmに上記で作成した仮想環境を紐づける

任意の場所にプロジェクトルートとなるフォルダを作成する
Pycharmでそのフォルダを開く

Settingを選択
f:id:shinseidaiki:20211031191119p:plain

Project>Python Interpreter 画面の歯車マークからAddを選択する

Existing environmentを選択して、...マークを選択する

%USERPROFILEフォルダ以下で envs\[仮想環境の名前]\python.exe を選択する
例: envs\DRF_Youtube\python.exe

すべてOKでPyCharmを再起動すると適用される

4. プロジェクトの作成

backendフォルダを作りその直下で以下の作業をする

プロジェクト名はconfigとしておく

django-admin startproject config .

アプリ名は基本的にはapiとする

django-admin startapp api

5. サーバーの起動

manage.pyを右クリックしてrun 'manage'をクリック
右上にあるボタンで以下のようにEdit Configurationsを選択する
f:id:shinseidaiki:20211031192550p:plain

parametersの箇所に runserver を追記する


この状態で再生ボタンを押すことでサーバーを起動させることができる

6. setting.pyを編集

import os

を記載


INSTALLAPPにパッケージを追記していく

基本は以下でそれ以外は必要なものをどんどん追記していく

'rest_framework',
'api.apps.ApiConfig',
'corsheaders',
'djoser',


corsに関してはMIDDLEWAREにも記載

'corsheaders.middleware.CorsMiddleware',


corsに関しては他にも許可するフロント側の接続元を指定しなければいけない
Reactでローカル環境で接続する場合

CORS_ORIGIN_WHITELIST = [
    'http://localhost:3000'
]

認証に関する権限に関しては、通常Viewに各画面毎に定義するが、基本となる認証ロジックに関してはsetting.pyに書き、差分をViewで適用する方法がベストプラクティスとなる
今回はJWTの認証を使用する
Tokenの有効期限も決定できる

from datetime import timedelta



REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

SINMPLE_JWT = {
    'AUTH_HEADER_TYPES': ('JWT',),
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30)
}


Mediaファイルの設定

backend直下にmediaフォルダを作成し、以下を追記

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'


カスタマイズユーザーを使用

AUTH_USER_MODEL = 'api.User'

7. URLの設定

以下を足す

from django.conf.urls import include
from django.conf.urls.static import static
from django.conf import settings

urlpatterns に

path('api/', include('api.urls')),
path('authen/', include('djoser.urls.jwt')),

メディアを使用できるようにする

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

8. Modelの設定

カスタムUserを使用可能にする.
uuidをidに使うのが一般的だとされる

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
import uuid


class UserManager(BaseUserManager):

    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('Email address is must')

        user = self.model(email=self.normalize_email(email), **extra_fields)
        user.set_password(password)
        user.save(using=self._db)

        return user

    def create_superuser(self, email, password):
        user = self.create_user(email, password)
        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)

        return  user



class User(AbstractBaseUser, PermissionsMixin):

    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    email = models.EmailField(max_length=255, unique=True)
    username = models.CharField(max_length=255, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = 'email'

    def __str__(self):
        return self.email


他は作りたいモデルを作る
例:

class Video(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    title = models.CharField(max_length=30, blank=False)
    video = models.FileField(blank=False, upload_to=load_path_video)
    thum = models.ImageField(blank=False, upload_to=load_path_thum)
    like = models.IntegerField(default=0)
    dislike = models.IntegerField(default=0)

    def __str__(self):
        return self.title

uploadされるファイル、画像の名前は関数化しておくのが便利

def load_path_video(instance, filename):
    return '/'.join(['video', str(instance.title)+str('.mp4')])

def load_path_thum(instance, filename):
    ext = filename.split('.')[-1]
    return '/'.join(['thum', str(instance.title)+str('.')+str(ext)])

9. urls.pyの設定

アプリの方のurls.py

routersでルーティングの設定を行う

from rest_framework import routers
from django.urls import path
from django.conf.urls import include

routers = routers.DefaultRouter()

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

10. マイグレーション

py .\manage.py makemigrations 
py .\manage.py migrate

11. Adminの設定

管理画面で使いたいモデルをインポートして、レジスターする
一覧画面で見られる項目増やしたりとかインライン追加したりとかもできる(参考:)

from .models import Video

admin.site.register(Video)


ユーザの場合は手間があり、以下のように上書きする
list_display - 一覧で表示できる項目
fieldsets - 詳細・編集画面で表示できる項目
add_fieldsets - 追加画面でのフォーム

from .models import User
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext as _

class UserAdmin(BaseUserAdmin):
    ordering = ['id']
    list_display = ['email', 'password']
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        (_('Personal Info'), {'fields': ('username',)}),
        (
            _('Permissions'),
            {
                'fields': (
                    'is_active',
                    'is_staff',
                    'is_superuser',
                )
            }
        ),
        (_('Important dates'), {'fields': ('last_login',)}),
    )
   add_fieldsets = (
        (None, {
           'classes': ('wide',),
           'fields': ('email', 'password1', 'password2'),
        }),
    )

admin.site.register(User, UserAdmin)


スーパーユーザーの作成

py manage.py createsuperuser

11. シリアライザーの作成

アプリにserializers.pyファイルを作成する

Userシリアライザーをつかう
Metaを使用することで使用するモデルのクラスやそのクラスの使いたいフィールドを設定できる
extra_kwargs はさらに制約を課すことができる(読み取り専用や文字数制限etc.)

from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import User, Video

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = get_user_model()
        fields = ('email', 'password', 'username', 'id')
        extra_kwargs = {'password': {'write_only': True, 'min_length': 5}}

    def create(self, validated_data):
        user = get_user_model().objects.create_user(**validated_data)
        return user

createメソッドの中のcreate_user(**validated_data)はユーザーモデルクラスの中のcreate_userを呼び出しており、その中のset_password(password)は渡されたパスワードをハッシュ化して設定するメソッドとなっており、パスワードはハッシュ化されて保存している

ユーザー以外のシリアライザーメソッドは基本Metaをいじっているだけで事足りる
なお、特定のレコードだけ取り出したい場合のコード作成例は以下

from core.models import Message, User, FriendRequest
from django.db.models import Q


class FriendsFilter(serializers.PrimaryKeyRelatedField):

    def get_queryset(self):
        request = self.context['request']
        friends = FriendRequest.objects.filter(
            Q(requested_user=request.user) & Q(approved=True)) 
        list_friend = []
        for friend in friends:
            list_friend.append(friend.requesting_user.id)
        queryset = User.objects.filter(id__in=list_friend)
        return queryset


class MessageSerializer(serializers.ModelSerializer):

    receiver = FriendsFilter()  # ←特定のレコードのみreceiverに格納したい

    class Meta:
        model = Message
        fields = ('id', 'sender', 'receiver', 'message')
        extra_kwargs = {'sender': {'read_only': True}}

12. Viewの作成

アプリのviews.pyを編集

ユーザー作成ビュー
汎用APIビューを使用generics.CreateAPIView

from rest_framework import generics
from .serializers import UserSerializer
from rest_framework.permissions import AllowAny

class CreateUserView(generics.CreateAPIView):
    serializer_class = UserSerializer
    permission_classes = (AllowAny,)

パーミッションは誰でも可能としている
これはsettings.pyの REST_FRAMEWORK に記述されたパーミッションが継承適用されると認証されていないユーザーが新規作成できなくなるためである
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated',] は認証しているユーザーだけがViewを見られる
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework_simplejwt.authentication.JWTAuthentication',]は認証の方法はJWTを使用する


通常のモデルView
viewsets.ModelViewSetはCRUDすべてを含む

from rest_framework import viewsets
from .serializers import VideoSerialzer
from .models import Video

class VideoViewSet(viewsets.ModelViewSet):
    queryset = Video.objects.all()
    serializer_class = VideoSerialzer


13. 作成したViewのURL制御


APIViewの汎用Viewで作成したviewはurlpatterns内に直接記載し、

  path('create', CreateUserView.as_view(), name='create'),

viewsetで作成したviewはrouterで紐づけすることができる
routers.registerの第一引数はURLのパス名となる

from .views import VideoViewSet, CreateUserView

routers.register('videos', VideoViewSet)

14. APIの確認

ユーザー作成は作れるかのチェック
http://127.0.0.1:8000/api/create

authenのエンドポイントはトークンを渡してもらうエンドポイント
トークンを調べたいときはポストマンを使用するとよい
jwtを使用する場合はPOSTメソッドで
エンドポイントhttp://127.0.0.1:8000/authen/jwt/create
body>form-dataを選択して
email: [ユーザーメール]
password: [ユーザーパスワード]
を入力してsendを押下

返ってきたjsonの中のaccessの要素がトークンとなる
取得したトークンをブラウザに保持させる
※認証情報を保持して確認する場合はクローム拡張機能のmodheaderを使用するとよい
モッドヘッダーを使用する場合は
RequestHeaderとして
KeyにAuthorization
ValueにJWT[半角スペース][取得したトークン]
f:id:shinseidaiki:20211101003100p:plain


http://127.0.0.1:8000/api/video


ここまで確認出来たらバックエンドの初歩的な実装完了


Frontendの実装 React

1. React プロジェクト作成

frontendフォルダをバックエンドと並列のフォルダ階層でフォルダを作成する
VSCodeでfrontendフォルダを開く
Terminalで

npx create-react-app ./

サーバーを起動する

npm start

1-2. エディタ環境構築

VSCodeでes7とprettierをインストールしておく

プリティアの設定
はグル、あ
Editor Format On Saveにチェックを入れるとセーブ時にインデント補正がかかる


2. google Material UIを使用する

MUI: The React component library you always wanted

アイコン関係インストール

npm install @material-ui/core
npm install @material-ui/icons

npm install react-icons


バックエンドとやり取りするためのCookieを使用できるようにする

npm install react-cookie

3. テーマの設定

App.js
テーマカラーのインポート

import { createTheme } from '@material-ui/core';
import { ThemeProvider as MuiThemeProvider } from '@material-ui/styles';

テーマの作成

const theme = createTheme({

})


Appコンポーネント内にthemeを適用

function App() {
  return (
    <MuiThemeProvider theme={theme}>
      
    </MuiThemeProvider>
  );
}


google fontsを使用する場合はgoogle fontsのCDN用URLを取得して、App.cssに追記してインポートすればどこでも呼び出せるようになる

4. NavBarの定義

componentsフォルダがない場合はsrc直下に作成する
contextフォルダも同様に作成する

components/NavBar.jsxファイルを作成する
rafceのテンプレートを使用してテンプレートを作成する

はApp()の中に配置する

function App() {
  return (
    <MuiThemeProvider theme={theme}>
      
      <NavBar />

    </MuiThemeProvider>
  );
}


必要なモジュールのインポート

makeStylesはほぼ使うはず

import { makeStyles } from '@material-ui/core';
[↓必要な場合はimport]
import { AppBar } from '@material-ui/core';
import { Toolbar } from '@material-ui/core';
import { Typography } from '@material-ui/core';


makeStylesの使い方は
関数外でuseStylesを定義して使用する

const useStyles = makeStyles((theme) => ({
}));

ファンクショナルコンポーネントの中でclassesとして定義して使用する

const classes = useStyles();

後は使用したい場所でclassesのフィールドを呼ぶだけ


5. 実装

省略

6. ルーティング router

インストール

npm install react-router-dom

index.jsに編集
ルーターCookieと自作するコンポーネントをインポートする

import { Route, BrowserRouter } from 'react-router-dom';
import { CookiesProvider } from 'react-cookie';

import [自作コンポーネント] from './components/*';

の中に BrowserRouter, Cookie, を入れて、その下にルーティングのアップコンポーネントを継ぎ足していく

ReactDOM.render(
  <React.StrictMode>
  <BrowserRouter>
  <CookiesProvider>

  <Route exact path='/' component={Login} />
  <Route exact path='/app' component={App} />
  
  </CookiesProvider>
  </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

7. ログインコンポーネント

componentsフォルダにLogin.jsx作成


materiat UI のテンプレートを使用する
https://mui.com/getting-started/templates/

講義ではmakeStylesを使用しているが、最新のMaterial UIはmakeStylesを使用していないコードが公開されている

テンプレートから拝借する分に加えてバックエンドと連携するのでaxiosやwithCookiesをインポートしておく

import { withCookies } from 'react-cookie';
import axios from 'axios';


モジュールの足りないエラーが出る場合はインポートする

npm install axios

テンプレートで使用している
@mui/material
@mui/icons-material
モジュールではエラーが出るため、
講義で使用している@material-ui/coreのモジュールのアイコンを使用する


useReducerを使用する 7-3へ



7-2. ログアウト機能

NavBarに追記
Cookieの使用方法
インポートするのと、exportクラスをwithCookiesで囲う必要がある

import { withCookies } from 'react-cookie';

export default withCookies(NavBar)


ログアウト機能の実装
propsを受け取り、cookiesを削除する
jwt-tokenはトークンを保持している変数

    const Logout = () => {
        props.cookies.remove('jwt-token');
        window.location.href = '/';
    }

クリックイベントに設定

<button className='logout' onClick={()=>Logout()}>

7-3. useReducerの実装


コンポーネント内にactionTypeを作成しておく
Reducerが使用するアクションの判定に利用する

インポートする

import React, { useReducer } from 'react'


初期状態

const initialState = {
    isLoading: false,  // FETCH中かどうか
    isLoginView: true, // アカウント作成かログイン画面かの切り替え
    error: '',
    credentialsLog: { // 認証情報を保有する
        email: '',
        password: '',
    }
}


Reducerを定義する
ationTypeによって上記の状態を変化させていく
例:START_FETCHが呼ばれた場合、ローディングを初めて、フェッチが成功した場合はローディングを終える
INPUT_EDITのaction.inputNameはemailやpasswordのどちらかがフォームで選択されているものが入る

const loginReducer = (state, action) => {
    switch (action.type) {
        case START_FETCH: {
            return {
                ...state, 
                isLoading: true,
            };
        }
        case FETCH_SUCCESS: {
            return {
                ...state, 
                isLoading: false,
            };
        }
        case ERROR_CATCHED: {
            return {
                ...state,
                error: 'Email or password is no correct!',
                isLoading: false,
            };
        }
        case INPUT_EDIT: {
            return {
                ...state,
                credentialsLogin: {
                    ...state.credentialsLogin,
                    [action.inputName]: action.payload,
                },
                error: '',
            };
        }
        case TOGGLE_MODE: {
            return {
                ...state, 
                isLoginView: !state.isLoginView,
            };
        }
        default: 
            return state;
    }

};


Loginファンクションの中でreducerを使用する
reducerはdispatchを使用してステータスを変化させる

7-4. ログインコンポーネントの画面

テンプレートを拝借する
useStylesを使っているとスタイルがあたらないところがあったので、その部分はテンプレートにあわせた

<Container maxWidth="xs">
            <form onSubmit={login}>
                <div className={classes.paper}>
                    {state.isLoading && <CircularProgress />}
                    <Avatar className={classes.avatar}>
                        <LockOutlinedIcon />
                    </Avatar>
                    <Typography variant="h5">
                        {state.isLoginView ? 'Login' : 'Register'}
                    </Typography>

                    <TextField 
                        variant="outlined" margin="normal"
                        fullWidth label="Email"
                        name="email"
                        value={state.credentialsLogin.email}
                        onChange={inputChangeLogin()}
                        autoFocus />

                    <TextField 
                        variant="outlined" margin="normal"
                        fullWidth label="Password"
                        name="password"
                        value={state.credentialsLogin.password}
                        onChange={inputChangeLogin()}
                        type="password" />

                    <span className={classes.spanError}>{state.error}</span>

                    { state.isLoginView ?
                        !state.credentialsLogin.password || !state.credentialsLogin.email 
                        ? <Button className={classes.submit} type='submit' fullWidth disabled 
                            variant='contained' color='primary'>Login</Button>
                        : <Button className={classes.submit} type='submit' fullWidth 
                            variant='contained' color='primary'>Login</Button>
                    :
                        !state.credentialsLogin.password || !state.credentialsLogin.email 
                        ? <Button className={classes.submit} type='submit' fullWidth disabled 
                            variant='contained' color='primary'>Register</Button>
                        : <Button className={classes.submit} type='submit' fullWidth 
                            variant='contained' color='primary'>Register</Button>
                    }

                    <span onClick={()=>toggleView()} className={classes.span}>
                        {state.isLoginView ? 'Create Account' : 'Back to Login'}
                    </span>

                </div>
            </form>            
        </Container>


cookieを使う場合ははexportをwithCookiesを囲い、propsで受け取る


7-5. ログインコンポーネントの中で呼び出されるメソッド


メールとかパスワードの入力フォームのonChangeのメソッド

const inputChangeLogin = () => (event) => {
        dispatch({
            type: INPUT_EDIT,
            inputName: event.target.name, // テキストフィールドフォームのnameが入ってくる email or password 
            payload: event.target.value,
        });
    };


loginのボタンが押された場合の処理

const login = async(event) => {
        event.preventDefault()
        if (state.isLoginView) {
            try { /  ログインの処理
                dispatch({type: START_FETCH})
                const res = await axios.post(`${HOST_NAME}/authen/jwt/create/`, state.credentialsLogin, {
                    headers: {'Content-Type': 'application/json'}})
                props.cookies.set('jwt-token', res.data.access);
                // アクセスが成功した場合は次の画面に遷移し、失敗した場合はLogin画面にとどまる
                res.data.access ? window.location.href = '/youtube' : window.location.href = '/';
                dispatch({type: FETCH_SUCCESS})
            } catch {
                dispatch({type: ERROR_CATCHED})
            }
        } else { // アカウント作成の場合
            try {
                dispatch({type: START_FETCH})
                await axios.post(`${HOST_NAME}/api/create/`, state.credentialsLogin, {
                    headers: {'Content-Type': 'application/json'}})
                // 上と同じログインの処理をする
            } catch {
                dispatch({type: ERROR_CATCHED})
            }
        }
    };

event.preventDefault()はボタンが押された場合に呼ばれるonSubmitの処理が走る際に画面の再読み込みが起こることを防ぐ目的で入れている


ログインとアカウント作成の切り替え処理

const toggleView = () => {
        dispatch({type: TOGGLE_MODE})
    }

7-6. ログイン機能の動作確認

F12開発ツールのapplication>Cookiesの欄にログイン中はトークンが入ったりログアウトすると消えることを確認する


7-x. インジケータの実装

reducerとの合わせ技になってくる
ファンクショナルコンポーネントの中ではステートに合わせて表示させたいコンポーネントを記述しておけばいい

{state.isLoading && <CircularProgress />}

Mainコンポーネントの定義


8. ApiContextの実装

contextフォルダ下にApiContext.jsxを作成
このコンポーネントには各画面で使用するApiを作り、呼び出し側はでくくって使う

まず初めの準備

インポート

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

Cookieを使えるようにする(propsとwithCookies)

import { withCookies } from 'react-cookie';

const ApiContextProvider = (props) => {
    return (
        <div>
            
        </div>
    )
}

export default withCookies(ApiContextProvider)

ApiContextの生成

export const ApiContext = createContext()


Stateの定義
tokenはProviderで保持する
必要なステートを作成する

const token = props.cookies.get('jwt-token');
const [videos, setVideos] = useState([]); 
... 他多数


useEffectの実装
アプリ起動時(初期画面読み込み時)に実行されるように設定
トークンが更新した場合も再読み込みされる

useEffect(() => {
        const getVideos = async() => {
            // 通常はFetchなどの処理
        }
        getVideos();
    }, [token]);

AxiosによるFetchの処理
Autherizationにトークンを持たせる
今回はJWTであり、'rest_framework.authtoken',を使用している場合は`Token ${token}`の記述となる

const res = await axios.get(`${HOST_NAME}/api/videos`, {
                    headers: {
                        'Autherization': `JWT ${token}`
                    }});
                setVideos(res.data)

setVideosは取得したデータをVideosのステートにセットする
これらはエラーが起こりうるのでトライキャッチする


フォームから送られてきたデータをバックエンドに渡す処理(追加の処理)
FormDataインスタンスを生成して、appendでフィールドを追加していく
axiosの第二引数にフォームのデータ(uploadData)を渡すのを忘れないように
Autherizationは文字列ではない可能性がある

const newVideo = async() => {
        const uploadData = new FormData()
        uploadData.append('title', title)
        uploadData.append('videos', videos, video.name)
        uploadData.append('thum', thum)
        try { 
            const res = await axios.post(`${HOST_NAME}/api/videos/`, {
                headers: {
                    'Content-Type': 'application/json',
                    'Autherization': `JWT ${token}`,
                }
            })
            setVideos([...videos, res.data]);
            // アップロードが終わったら初期化の処理
    setModalIsOpen(false);
            setTitle('');
            setVideo(null);
            setThum(null); 
        } catch {
            console.log('error');
        }
    }

setVideos([...videos, res.data])は追加したデータをローカルにも反映させる記述上のテクニックの一つで、...videosの三点リーダーは配列を展開のさせる記法
(バックエンド上に追加したデータを明示的に手元のステートの一番最後の要素として追加している)
アップロードの処理が終わったら各種のステートを初期化する


フォームから送られてきたデータをバックエンドに渡す処理(削除の処理)
削除の場合はどのデータを削除するかのidをURLで渡すところが特徴となる
削除の場合はaxiosに特にフォームデータを送らない

const deleteVideo = async() => {
        try { 
            await axios.delete(`${HOST_NAME}/api/videos/${selectedVideo.id}`,  {
                headers: {
                    'Content-Type': 'application/json',
                    'Autherization': `JWT ${token}`,
                }
            });
            // 初期化する処理
            setSlectedVideo(null);
            setVideos(videos.filter((item) => item.id !== selectedVideo.id))
        } catch {
            console.log('error');
        }
    };

setVideos(videos.filter*1は今削除したデータを手元のステートからも破棄している処理
(reactは明示的に破棄しなければならない模様)


いいね機能
既存データに関して、ある値だけを更新させたい場合の処理はpatchを使用する
(updateは明示的にフィールドをすべて記載しない場合、ほかのフィールドを全部nullで上書きするのかもしれない?)

const incrementLike = async() => {
        try {
            const uploadData = new FormData();
            uploadData.append('like', selectedVideo.like + 1);

            const res = await axios.patch(`${HOST_NAME}/api/videos/${selectedVideo.id}`, uploadData, {
                headers: {
                    'Content-Type': 'application/json',
                    'Autherization': `JWT ${token}`,
                }
            });
            setSlectedVideo({...selectedVideo, like: res.data.like});
            setVideos(videos.map(item => (item.id === selectedVideo.id ? res.data : item )))
        } catch {
            console.log('error');
        }
    }

setSlectedVideo({...selectedVideo, like: res.data.like});は更新後のlikeの値だけ書き換えたい処理
setVideos(videos.map(item => (item.id === selectedVideo.id ? res.data : item )));は更新したアイテムだけを更新した値で上書きする処理
(それ以外は元の値を代入しているので変化なし)


他のコンポーネントからApiContextを利用できるようにする
return以下にの属性にステートと、メソッドをひたすら記載することで、ほかのコンポーネントから呼び出せるようになる
はApiContextProviderの外でcreateされたコンテキストの模様

return (
        <ApiContext.Provider
            value={{
                videos,
                title,
                setTitle,
                ... 他多数ステート、ステートセット関数
                newVideo,
                deleteVideo,
                ... 他多数メソッド           
           }}
        >
            {props.children}
        </ApiContext.Provider>
    );

{props.children}はApp.jsのreturnの中にdivタグなどの何らかのタグが付与されたときに、無視してしまうことを防ぐためのもの


App.jsにApiContextの情報を記載してプロバイダーを使えるようにする
を一番上の階層で囲う

import ApiContextProvider from './context/ApiContext';


return (
    <ApiContextProvider>
      <MuiThemeProvider theme={theme}>
      
        <NavBar />

      </MuiThemeProvider>
    </ApiContextProvider>
  );

9. Main.jsxの作成

componentsフォルダに作成

Contextを使用できるようにする

import React, { useContext } from 'react';
import { ApiContext } from '../context/ApiContext';

他に必要なものをインポートする
GridはBootStrapのグリッドシステム
Fabはフローティングアクションボタンのこと

import Modal from 'react-modal';
import { makeStyles } from '@material-ui/core';
import { Grid, Container, Fab, AddIcon, Typography, TextField, IconButton } from '@material-ui/core';
import { Add } from '@material-ui/icons'
import { IoMdClose, RiUploadCloud2Line } from 'react-icons';
import { FaVideo } from 'react-icons/fa';
import { BsImages } from 'react-icons/bs';


Modalを使う場合のおまじない Modal.setAppElement('#root');
このrootはpublic/index.htmlのbodyの中のdivのidを差している
カスタムスタイルも作成しておく

const Main = () => {
    
    Modal.setAppElement('#root');


   const customStyles = {
        content: {
            top: '30%',
            left: '43%',
            right: 'auto',
            bottom: 'auto',
        },
    };

    return (

インストールもわすれずに

npm install react-modal


コンテキストを使用する
必要なメソッドとステートを使いたいものだけ使用する

const {
        title,
        setTitle,
        video,
        setVideo,
        thum,
        setThum,
        modalIsOpen,
        setModalIsOpen,
        newVideo,
    } = useContext(ApiContext);


Gridスタイルでレイアウトの大枠を作成する
BootStrapは画面を12等分しており、数値は12等分のうちの何個分を使うかを指定できる

return (
    <>
        <Grid container className={classes.grid}>
            <Grid item xs={11}>
                <Grid container spacing={5}>
                    <Grid item xs={12}></Grid>

                    <Grid item xs={1}>
                        <Fab color='primary' aria-label='add' onClick={()=>setModalIsOpen(true)}>
                            <Add />
                        </Fab>
                    </Grid>

                    <Grid item xs={8}>
                        <VideoDetail />
                    </Grid>
                    
                    <Grid item xs={3}>
                        <VideoList />
                    </Grid>
                </Grid>
            </Grid>
        </Grid>
    </>
    )


アプリに反映させるために

をApp.jsに記載

import Main from './components/Main';

return (
    <ApiContextProvider>
      <MuiThemeProvider theme={theme}>
      
        <NavBar />
        <Main />

      </MuiThemeProvider>
    </ApiContextProvider>
  );

9-1 コンテンツの一覧表示画面、詳細画面、両方に共通する部分の画面となるコンポーネントの作成をする

VideoList.jsx 一覧画面
VideoDetail.jsx 詳細画面
VideoItem.jsx 共通部品


Mainに一覧画面と詳細画面を呼び出させる処理

<Grid item xs={8}>
    <VideoDetail />
</Grid>
                    
<Grid item xs={3}>
     <VideoList />
</Grid>

9-2. コンテンツを追加できる処理

Modalを使って動画をアップロードする
input type="file"の部分でhiddenにしているのは、シンプルな送信ボタンが表示されてデザイン的によくないので隠す意図(モダンなアイコンを利用したいため)
ModalのonRequestClose={()=>setModalIsOpen(false)}はモーダル画面外をクリックされたらモーダルを閉じる設定にしている
テキストフィールドはセットステートとの組み合わせるとシンプルに記載できる
setTitle(event.target.value)} />
Containrはボタンを押したときにファイルをアップロードする処理を記載している

<Modal isOpen={modalIsOpen}
            onRequestClose={()=>setModalIsOpen(false)}
            style={customStyles}
        >
            <Typography>Movie title</Typography>
            <br />
            <TextField type='text' onChange={(event) => setTitle(event.target.value)} />
            <br />
            <br />
            <Container className={classes.container}>
                <input type="file"
                    id="mp4input"
                    hidden="hidden"
                    onChange={event => setVideo(event.target.files[0])} 
                />

                <IconButton onClick={handleEditMovie}>
                    <FaVideo className="photo"/>
                </IconButton>
            </Container>
        </Modal>

動画ファイルを実際にアップロードするのはhiddenにされたinputタグで、画面上ではIconButtonをユーザーが押したら、このinputタグを押すように作ってある
(inputのデフォルトアイコンを表示させてしまうと微妙なアイコンなので、モダンなアイコンだけを画面上に表示したいため)

const handleEditMovie = () => {
        const fileInput = document.getElementById("mp4Input")
        fileInput.click();
}
const handleEditPicture = () => {
        const fileInput = document.getElementById("imageInput")
        fileInput.click();
    }


最終的にアップロードするボタンとキャンセルするボタンの設置
テキスト・動画・画像のすべてがセットされているときのみOKが押せる

                { title && video && thum && (
                    <button className="btn-modal" onClick={() => newVideo()}>
                        <RiUploadCloud2Line />
                    </button>
                )}
                <button className="btn-modal" onClick={() => setModalIsOpen(false)}>
                    <IoMdClose />
                </button>

CSSについて、
アイコンの背景を消すのは background-color: transparent;
縁消しはborder: none; outline: none;
カーソルはcursor: pointer;



10 動作確認


意外とCORSのエラーが出て苦労した
pointとしては以下二つ

Authorizationは''でくくる

'Authorization': `JWT ${token}`,

ModHeaderは切る


11 コンテンツの実装


一覧画面・詳細画面で表示させる共通要素のコンポーネントxxItem.jsxを実装する
各コンテンツ一つ一つをもらうために引数にコンテンツのオブジェクトをもらう
あとはコンポーネントの中身を実装して、一覧画面を作る

const VideoItem = ({video}) => {
    return (


一覧画面の実装
ApiContextからステートを借りてくる(Videos)
マップでリストに展開する変数に格納する(listOfVideos)
その変数を画面に表示させる

const VideoList = () => {
    const { videos } = useContext(ApiContext);
    const listOfVideos = videos.map((video) => (
        <VideoItem key={video.id} video={video} />
    ));
    return (
        <Grid container spacing={5}>
            <div className="video-list">{listOfVideos}</div>
        </Grid>
    )
}


詳細画面の実装
ApiContextから必要なメソッドを使用する

const {
        selectedVideo,
        deleteVideo,
        incrementLike,
        incrementDislike
    } = useContext(ApiContext);

コンテンツ選択中とコンテンツ未選択の場合で返すreturnを変える

if (!selectedVideo)
        return (
            <div className="container">
                <button className="wait">
                    <IoLogoYoutube />
                </button>
            </div>
        );

    return (
        <>
        </>
    );

12 動画を再生させる

react-playerというパッケージを使う

npm install react-player

インポート

import ReactPlayer from 'react-player';


プレイヤーのコンポーネント

              <ReactPlayer
                    className="player"
                    url={selectedVideo.video}
                    width="100%" height="100%"
                    playing controls
                    disablePictureInPicture
                    config={{
                        file: {
                            attributes: {
                                controlsList: "nodownload",
                                disablePictureInPicture: true,
                            },
                        },
                    }}
                />

playingは自動で再生が始まる
controlsをつけておくと再生ボタンや停止ボタンが表示される
disablePictureInPictureはスクショを撮る機能
controlsList: "nodownload",はデフォルトでダウンロードできる機能を無効化している

*1:item) => item.id !== selectedVideo.id