20201006のReactに関する記事は7件です。

React チュートリアル 1日目

今日からReactチュートリアルをはじめました。

ちょっと前にやってみたが、
うろ覚えだったため、再度挑戦。

メモ代わりにその日勉強したことを書いていこうと思います。

誤った部分などございましたら、ご指摘いただけると幸いです。

参考記事
https://qiita.com/o_sol06/items/dce7b6c99ecb3abf7ec1#%E7%92%B0%E5%A2%83


Reactとは

React はユーザインターフェイスを構築するための、宣言型で効率的で柔軟な JavaScript ライブラリです。
複雑な UI を、「コンポーネント」と呼ばれる小さく独立した部品から組み立てることができます。


1.環境構築
前回はブラウザ内で開発したため、今回はローカル開発環境で作成

ターミナルでnode.jsがインストールされているか確認

node -v

2.新規Appの作成

npx create-react-app my-app

3.src/フォルダの中のファイルをすべて削除(画像)

cd my-app
 cd src
 rm -f *
 cd ..

4.index.cssという名前のファイルを、src/フォルダに作成し、以下の内容を記述します。

index.css
    body {
      font: 14px "Century Gothic", Futura, sans-serif;
      margin: 20px;
    }

    ol, ul {
      padding-left: 30px;
    }

    .board-row:after {
      clear: both;
      content: "";
      display: table;
    }

    .status {
      margin-bottom: 10px;
    }

    .square {
      background: #fff;
      border: 1px solid #999;
      float: left;
      font-size: 24px;
      font-weight: bold;
      line-height: 34px;
      height: 34px;
      margin-right: -1px;
      margin-top: -1px;
      padding: 0;
      text-align: center;
      width: 34px;
    }

    .square:focus {
      outline: none;
    }

    .kbd-navigation .square:focus {
      background: #ddd;
    }

    .game {
      display: flex;
      flex-direction: row;
    }

    .game-info {
      margin-left: 20px;
    }

5.index.jsという名前のファイルを、src/フォルダに作成し、以下の内容を記述します。

index.js
 class Square extends React.Component {
      render() {
        return (
          <button className="square">
            {/* TODO */}
          </button>
        );
      }
    }

    class Board extends React.Component {
      renderSquare(i) {
        return <Square />;
      }

      render() {
        const status = 'Next player: X';

        return (
          <div>
            <div className="status">{status}</div>
            <div className="board-row">
              {this.renderSquare(0)}
              {this.renderSquare(1)}
              {this.renderSquare(2)}
            </div>
            <div className="board-row">
              {this.renderSquare(3)}
              {this.renderSquare(4)}
              {this.renderSquare(5)}
            </div>
            <div className="board-row">
              {this.renderSquare(6)}
              {this.renderSquare(7)}
              {this.renderSquare(8)}
            </div>
          </div>
        );
      }
    }

    class Game extends React.Component {
      render() {
        return (
          <div className="game">
            <div className="game-board">
              <Board />
            </div>
            <div className="game-info">
              <div>{/* status */}</div>
              <ol>{/* TODO */}</ol>
            </div>
          </div>
        );
      }
    }

    // ========================================

    ReactDOM.render(
      <Game />,
      document.getElementById('root')
    );

6.index.jsの先頭に、以下の行を追加します。

index.js
 import React from 'react';
   import ReactDOM from 'react-dom';
   import './index.css';

ターミナル上でnpm startを実行し、ブラウザで
http://localhost:3000
へアクセスすると、三目並べゲームのフィールドが表示される


用語まとめ

component = React により最終的に出力する JSX を構成する再利用可能な部品のこと

render = 描画すべきものの軽量な記述形式である React 要素というものを返す

props = 親要素から与えられる値

this.state = それが定義されているコンポーネント内でプライベートと見なすべきもの


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reduxの簡単なコードを書きながら、処理の流れを整理してみた

はじめに

Reduxを最初に勉強する際に、まず登場する用語が、多くて、良く分からんとなりがちだと思います。少なくとも自分はそうです(泣)

まだまだ勉強中ではあるものの、一旦、整理する意味で、Reduxの処理の流れを言語化してみたいと思い、記事にまとめました。

※今は、Redux Toolkitを使うのが普通なのかもしれませんが、そこには触れていません。Redux Toolkitに関しては、追ってアウトプットしようと思っています。

Reduxを使用する中で登場する用語の整理

まず登場する用語の整理から...

State
アプリケーションの状態。

Store
Stateの状態を保持している場所。

Action
ユーザーがアプリケーションで何をしたいかという情報を持つプレーンなオブジェクト。
コンポーネントから発行されたActionは、Dispatcherによって、Reducerに渡す。

Reducer
Actionを元にStateを更新する副作用の無いメソッド。

Reduxの処理の流れ

整理する意味でも図化してみました。
① コンポーネントが、Action Creatorを呼び出し、Actionを取得します。
② 取得したActionをReducerに渡します。これをdispatchといいます。
③ Reducerは渡されたActionに応じてStateを更新します。
④ コンポーネントは、Stateに変更があった場合は、該当のUIを描画し直します。

何だか、手続きが、面倒くさいように感じますが、そこがポイントです。
決まった手続きでしか変更を加えられない、状態に対して定義された変更しか加えられないルールは、予期せぬ更新処理を防ぎ、コードを予測しやすくしてくれます。

Action Creatorも重要な役割を果たしています。Reducerに対しては決められた属性、値を持つActionを渡さなければいけないのですが、間違ったActionを渡さないよう、Action Creatorを介して、Actionを取得するようにします。

reduxFlow

では、実際にそれぞれの役割で書かれる処理を書いてみましょう。
よくあるカウンターですが、何となくイメージの手助けになれば、と思います。

ユーザーがアプリケーション上で何らかの操作をし、Actionが発行される

ユーザーの操作によって、コンポーネントから、Actionが生成されます。
後述しますが、コンポーネント上で、この定義ファイルをimportして利用することで、コンポーネントからActionの作成依頼をかけられます。

Actionというディレクトリを切って、そこにActionを発行するメソッドを定義しておきます。
以下は Action Creator と呼ばれるものです。dispatchする時に生の値を渡すのでなく、Action Createrの関数の戻り値を使います。これは、バグを防ぐために有効です。

Redux StyleGuide

//Actionはどんなイベントが起こったかを表現するプレーンなオブジェクトです

export const increment = () => {
  return {
    type: "INCREMENT"
  }
}

export const decrement = () => {
  return {
    type: "DECREMENT"
  }
}

actionをdispatchする際は、生の値を渡すのでなく、Action Creatorの戻り値を使う方が安全です。
※以下のように、生の値を渡さないこと。

<button onClick={()=> dispatch({type: "INCREMENT"})}>click</button>
<button onClick={()=> dispatch({type: "DECREMENT"})}>click</button>

以下のように、Action Creator関数の戻り値を使いましょう。

<button onClick={()=> dispatch(increment())}>click</button>
<button onClick={()=> dispatch(decrement())}>click</button>

発行されたactionをdispatchする

Actionは生成しただけだと意味がなくて、Dispatchしないと、Store内の値を変更することはできません。
コンポーネント内で、Actionをimportして、ActionをDispatchしましょう。

※Classコンポーネントが主流だった時代は、connect関数が利用されていたようですが、現在は、useSelectoruseDispatchを使って、ReactとReduxの接続が可能なので、こちらを積極的に使っていった方が良さそう。

↓useSelectorを使って、Storeからstate(値)を取得し、useDispatchにAction Creatorを渡して、ActionをDispatchします。

import React from 'react';
import { increment, decrement } from 'Action/actionCreator';
import { useSelector, useDispatch } from 'react-redux';

function App() {

  const count = useSelector(state => state.count);
  const dispatch = useDispatch()

  return(
    <>
    <div>{count}</div>
    <button onClick={()=> dispatch(increment())}>click</button>
    <button onClick={()=> dispatch(decrement())}>click</button>
    </>
  )
}

export default App;

DispatchされたActionによってReducerでStateを更新する

ここでは、Reducerディレクトリを切って、reducer.jsに以下の記述を書いています。
Reducerの関数は二つ引数をとります。第一引数にstate、第二引数にはactionがreturnした値をとります。
(ここでは、単純な処理しか書いてませんが、typeとpayloadを書けます)

第一引数のstateですが、基本的には現在の引数の状態を受け取るようになっていますが、もし現在のstateの状態が指定されていない場合は、デフォルトの値をstateの引数に持たせるようにします。

export const initialState = { count: 0 };

export const countReducer = (state = initialState.count, action) => {
  switch(action.type) {
    case 'INCREMENT':
      return {
        ...state, count: state.count + 1
      };
    case 'DECREMENT':
      return {
        ...state, count: state.count - 1
      }
    default: {
      return state;
    } 
  }
}

Storeを作成してpropsとして渡す

Reducerが作成できたら、Reduxの核となるStoreを初期化して、Propsとして渡しましょう。
Storeは1アプリケーションにつき、一つです。引数には、reducerと初期値を渡します。

const store = createStore(reducer, initialState);

最後にStoreを最上位のコンポーネントでProviderを使って、Storeをpropsとして渡します。これで初めて、子孫コンポーネントの中でReduxの機能が使えるようになります。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { initialState, reducer } from './Reducer/reducer';

const store = createStore(reducer, initialState);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

まとめ

以上で一通りのReduxに必要な処理が揃ったことになります。上述のuseSelectoruseDispatchによって、ReactとReduxの接続は実現できています。ユーザーのアクション(ボタンのクリックなど)の応答として、更新された値が表示されます。

まだまだ理解の浅い部分が沢山ありますが、Reduxのディレクトリ構成(Ducks,re-ducks等)や、もっとすっきり書ける方法(redux toolkit)もキャッチアップして理解を深めていきたいな思います(楽して書きたい)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React用語まとめ

今回はReactの基本的な用語をまとめます。
Reactで出てくる用語が分からない時の参考になれば幸いです。

SPA(シングルページアプリケーション)

これは、単一のHTMLページでアプリケーションの実行に必要なすべてのアセット(JavaScriptやCSSファイルなど)をロードすることができるアプリケーションです。ページのリロードが発生しないため、ユーザーが快適に使用することができます。
また、全てをシングルページアプリケーションで構築せずとも、既存のウェブサイトの一部分だけを拡張し使用することも可能です。

ES6、ES2015、ES2016 など

これらの頭字語は全て、ECMAScript言語の標準仕様の最近のバージョンのことを指しています。JavaScript言語とはこれらの仕様に対する実装です。また、アロー関数やクラス、テンプレートリテラル、let, constステートメントなどはES6バージョンで追加されており、できることの幅が大きく変化しています。

コンパイル

プログラミング言語を、動作する機械が理解できるように翻訳する作業のことです。

コンポーネント

ページに表示されるReact要素を返す、小さく再利用可能なコードのことです。再利用できることは、プログラミングにおいてとても重要です。

種類

class component

クラスによって定義されたコンポーネント

  • React.conponentを継承する。
  • ライフサイクルやstateをもつ。
  • propsにはthisが必要である。
  • renderメソッド内でJSXをreturnする。

functional component

関数型で定義されたコンポーネント

  • ES6のアロー関数で記述する。
  • stateをもたない。
  • propsを引数に受け取る。
  • JSXをreturnする。

props

Reactコンポーネントへの入力のことです。親コンポーネントから子コンポーネントへと順番に渡されるようなデータです。
また、読み取り専用なため、変更されるべきではありません。

state

コンポーネント内で管理する変数のことです。
あるコンポーネントが時間とともに変化するデータと関連付けられている場合はstateが必要です。
stateとpropsの大きな違いは、propsは親コンポーネントから渡されますが、stateはコンポーネント自身によって管理されるということです。コンポーネントは自身のpropsを変更することができませんが、state は変更することができます。

ライフサイクルメソッド

コンポーネントの様々なフェーズにおいて実行される特別な関数です。
コンポーネントの時間の流れは、生まれて、成長して、消えていくまでの循環です。それぞれの段階で必要な処理を記述します。

Mounting→Updating→Unmounting

これをなぜ使うのかと言いますと、関数の外に影響を与える関数を記述するためです。

最後に

Reactを学習する上で登場する基本的な用語を自分なりにまとめてみました。また、Reactの特徴の一つであるJSXについては別の記事にて書かせていただいておりますので、そちらも見て頂けたら幸いです。

URL : https://qiita.com/Taisei-rgb/items/137f24d6256d71bab90a

ここまで記事を読んでいただきありがとうございました。React学習中の方の少しでも参考になれば幸いです。今後もフロントエンドをメインにアウトプットをしていきますので、今後ともよろしくお願い致します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Contentfulとreactjsで簡易ブログを作る方法

概要

Headless CMSで有名なContentfulでブログっぽいものの作り方を紹介します。

サンプル

repo https://github.com/koji/typescript/tree/master/simple_blog_with_contentful

Screen Shot 2020-10-05 at 11.05.25 PM.png

Headless CMSとは

作り方について書く前に、Headless CMSとは何かを簡単に。  
ざっくりに言うと、コンテンツを表示する機能を持たないWordPressです。
利用者はHeadless(Contentful)でコンテンツを編集し、そのデータをAPI経由取得し、
表示する必要があります。

- メリット
- フロントエンドにリソースを集中できる
- フロントエンドとバックエンドは完全別個のシステムなので、フロントエンド開発に関して制約がない(好きなフレームワークを使える)
- ContentfulはAPIに簡単にアクセスするためのnpmパッケージを公開しているので、APIアクセス自体も簡単
- デメリット
- エンジニアではない人にとって、ちょっとした表示の変更が難しいことでしょうか
- バックエンドに問題が発生した場合、問題が解決されるまで待つ以外の選択肢がない

Contentfulについてもう少し詳しく知りたいという方はこちらをご覧になると良いかと思います。
https://fromscratch-y.work/ja/blog/technology/other/start-contentful/

Step 1 Contentfulでモデルを定義する

Contentfulにログインして、フロントエンド(react)で表示するためのデータの型を定義します。
今回はタイトル、アイキャッチ用の画像、本文と言う3つを用意しました。

データの構造はこんな感じです。JSONViewerで確認できます。
js
{
"name": "easysite", <-- コンテンツモデル名
"description": "simple website like a blog",
"displayField": "title",
"fields": [
{
"id": "title",
"name": "title",
"type": "Symbol",
"localized": false,
"required": false,
"validations": [],
"disabled": false,
"omitted": false
},
{
"id": "image",
"name": "image",
"type": "Link",
"localized": false,
"required": false,
"validations": [],
"disabled": false,
"omitted": false,
"linkType": "Asset"
},
{
"id": "description",
"name": "Description",
"type": "Text",
"localized": false,
"required": false,
"validations": [],
"disabled": false,
"omitted": false
}
],
"sys": {
"space": {
"sys": {
"type": "Link",
"linkType": "Space",
"id": "if4se75018sp"
}
},
"id": "easysite",
"type": "ContentType",
"createdAt": "2020-10-01T15:28:51.896Z",
"updatedAt": "2020-10-01T15:28:52.158Z",
"environment": {
"sys": {
"id": "master",
"type": "Link",
"linkType": "Environment"
}
},
"publishedVersion": 1,
"publishedAt": "2020-10-01T15:28:52.158Z",
"firstPublishedAt": "2020-10-01T15:28:52.158Z",
"createdBy": {
"sys": {
"type": "Link",
"linkType": "User",
"id": "0EGNAqGfBgN849uaItzT7r"
}
},
"updatedBy": {
"sys": {
"type": "Link",
"linkType": "User",
"id": "0EGNAqGfBgN849uaItzT7r"
}
},
"publishedCounter": 1,
"version": 2,
"publishedBy": {
"sys": {
"type": "Link",
"linkType": "User",
"id": "0EGNAqGfBgN849uaItzT7r"
}
}
}
}

Step 2 コンテンツ作成

コンテンツ作成と言っても、特に難しいことはなく、単純にContentful上で新しいエントリを作成するだけです。Markdown記法も使えます!

Content --> Add Entry --> easysiteをクリックすると下記が表示されます。

上から順に、タイトル、アイキャッチ画像、本文という並びです。
注意点は本文をPublishしても、アイキャッチの画像は一緒にPublishされないという点です。
個別にPublishする必要があります。(もしかしたら、一律Publishする方法もあるのかもしれませんが、調べてません。。。)

Step 3 API-Keyの取得

ContentfulのSettingsからAPI Keyを発行して、SpaceIDとAccess Tokenを取得します。

注意点は次のステップでDelivery APIを使った場合、Publishしてないコンテンツに対して、react appからAPIにアクセスするとデータが返ってこないことです。

Step 4 Reactアプリ作成

Step3でContentful上での作業は終了です。以降は通常のフロントエンドの作業になります。
Reactアプリの場合は下記の手順が楽だと思います。
1. create-react-app で雛形作成
2. API-key用のコンポーネント作成(サンプルではハードコードしちゃってますが、デプロイ環境、netlifyとかで環境変数をセットして取得するようにするのが、いいと思います。)
3. contentful を yarn/npmでインストール

https://www.npmjs.com/package/contentful

4. contentfulを利用して、コンテンツを取得
5. データをパースして、表示

サンプルではApp.tsxの中で、useEffect使って、content_typeを指定して、データをとってきています。
* anyとか入ってますが、型定義をサボっているだけなので、スルーして下さい。。。

useEffect(() => {
    fetchData();
    // console.log(articles);
  }, [articles]);

  const fetchData = async() => {
    try {
      const resp = await client.getEntries ({content_type: 'easysite'});
      setArticles(resp.items as any);
    } catch (error) {
      console.log(error);
    }
  }

エントリー表示部分
propsとして渡して、各々の変数からデータ取り出しています。
descriptionってのが本文です。今回はMarkdownでエントリを作っていので、markedを入れて、htmlに変換して、dangerouslySetInnerHTMLを経由して表示させています。
ts
export const Post = ({ article }: IArticle) => {
// console.log(article);
const { title, image, description} =article.fields;
const postDescription = marked(description);
return (
<div className="post">
<h2 className="title">{title}</h2>
{image && <img className="featuredImage" src={image.fields.file.url} alt={title} title={title} /> }
<section dangerouslySetInnerHTML={{ __html: postDescription}} />
</div>
)
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactとRailsを同一サーバーで動かす方法

経緯

Railsの学習を始めるに当たって、 ERBを利用したVIEWでフロント側を構築していたが、
最近はSPAというものが流行っていると小耳に挟んだ。

とりあえずチャレンジ!
ということでReactの学習を並行して進めていたが、、、
これどうやってデプロイするんだ。。。

となってしまいました笑

とりあえず別サーバーで動かすことにしてCORS制約などもなんとなくクリアして無事にRailsとReactのやりとりが可能になった。

しかし、ReactとRailsを利用したアプリ関係の記事を漁っていると、同一サーバーで動かしているものを多数発見!!!
何回かチャレンジしたものの上手く行かず断念。。。

しばらく時間が過ぎ、この土日に絶対動かしてやる!!! と執念で参考記事を探しまくった結果、
Heroku公式のBlogにわかりやすい記事があり、やっと構築ができました笑

同じような人がいたら参考になれば良いなと思い投稿するに至りました!

前提条件

  • Heroku blogの記事から必要最小限の部分だけ引用したものになります
  • Ruby、Rails、Node、npm OR yarnなどの基本的なインストールが済んでいること
  • Railsアプリを作成したことがある
  • Reactアプリをcreate-react-appで作成したことがある
  • Herokuでホスティングをしたことがある(gitでデプロイ)

参考記事:A Rock Solid, Modern Web Stack—Rails 5 API + ActiveAdmin + Create React App on Heroku

Railsアプリの作成

rails new コマンドでrails アプリを作成します。

--apiコマンドでapiモードにするのをお忘れなく

rails new rails_app --api

作成したアプリが起動するか確かめましょう。

cd rails_app
rails s

Reactアプリの作成

Railsアプリのルートディレクトリ上でclientという名前でReactアプリを構築します。

npm create-react-app client

Railsと同じように、Reactが動くか確かめましょう。

cd client
npm start 

このままだと開発中にReactとRailsで別ポートとなるので
package.jsonに設定を追加します。

package.json
{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:3001", #ここを追加
  ...
}

この設定によりRails側には、Reactアプリがポート3001で起動しているように認識させます。

React - Rails ローカル編

ローカル環境立ち上げ用のタスクを作成します。

ProcfileにReactとRailsを立ち上げるコマンドを書き、Railsのタスクで呼び出す方法にします。

Procfileはheroku上で利用されるファイルで、プロセスのタイプ:コマンドで記述できます。

<process type>: <command>

今回は、ルートディレクトリにProcfile.devというファイル名で、RailsアプリとReactアプリの起動コマンドを記述します。

Procfile.dev
web: cd client && PORT=3000 npm start
api: PORT=3001 && bundle exec rails s

呼び出しコマンドを簡潔にしたいため、taskにします。

start.rake
namespace :start do
    desc 'Start development server'
    task :dev do
      exec 'heroku local -f Procfile.dev'
    end
end

taskでprocfileの起動コマンドをキックします。

rake start:dev

ReactとRailsがどちらも立ち上がったと思います。

ターミナル上のログでわかりづらい場合はRails側で何かしらJsonを返すようにしておき、React側からfetchできるようにしておくとわかりやすいと思います。

React - Rails Heroku編

メインテーマであるHeroku上へのデプロイです。

まずはルートディレクトリにpackage.jsonファイルを作成します。

RailsのpublicディレクトリにReactの静的ファイルを作成するコマンドを記述します。

package.json
{
    "name": "list-of-ingredients",
    "license": "MIT",
    "engines": {
      "node": "10.15.3",
      "yarn": "1.15.2"
    },
    "scripts": {
      "build": "yarn --cwd client install && yarn --cwd client build",
      "deploy": "cp -a client/build/. public/",
      "heroku-postbuild": "yarn build && yarn deploy"
    }
}

production用のProcfileを作成し、railsの起動コマンドを記述します。

必要に応じてrelease後のmigrateコマンドも記述します。

Procfile
web: bundle exec rails s
release: bin/rake db:migrate



CLIを利用してheroku上のdynoの作成とビルドパックの設定を行います。

アプリの作成
heroku apps:create
ビルドパックの設定
heroku buildpacks:add heroku/nodejs --index 1
heroku buildpacks:add heroku/ruby --index 2

heroku上の準備ができたのでgitでpushします。

git add .
git commit -m "React - Rails"
git push heroku master

アプリを開きます。

heroku open

あとがき

みなさん無事に動きましたでしょうか?

もっと良い方法があれば是非教えていただけるとありがたいです。

もし動かない場合は下記の参考元の記事を読んでみてください!

参考記事:A Rock Solid, Modern Web Stack—Rails 5 API + ActiveAdmin + Create React App on Heroku

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【第2回】「みんなのポートフォリオまとめサイト」を作ります~SPA認証で死闘編~

はじめに

この連載記事は、僕が「みんなのポートフォリオまとめサイト」を作る過程をゼロから発信しながらみなさんに見てもらいつつ、作っている途中からみなさんにアドバイスをいただいて、よりよいサービスにしていきたいというお話です。

あとは「サービスを作っていく過程って初学者の人にとっては結構興味ある内容だったりするのでは?(少なくとも自分は知りたかった!)」と思い、このようなスタイルで記事を書いています。

一週間に一度のペースで更新しますとか言っちゃいましたが、前回更新からなんと1ヶ月半も経ってしまった!
今後も完成までゆるく更新していく予定です。

前回までの記事

【第0回】「みんなのポートフォリオまとめサイト」を作ります~宣言編~
「とりあえずこれから作るからみんな見てて!」と宣言しただけの記事。

【第1回】「みんなのポートフォリオまとめサイト」を作ります~着手編~
仕様を決めたり、サービス名考えたり、デザインを作ったり。

今回から、いよいよ実装に入っていきます。

これまでやったことと作業時間

やったこと 前回までの作業時間 ( h ) 今回の作業時間 ( h )
仕様決め(競合調査) 3
サービス名考案 2
手書きでWF作成 2
デザイン作成 8
DB設計 0 4
実装(環境構築) 0 4.5
実装 0 25
その他 2

作業時間計 ( h )

前回まで 今回 合計
17 33.5 50.5

1ヶ月半で33.5hという怠惰っぷり。何をしていたんだろう(前回も同じようなこと言ってる。)
Reactでアウトプットするのが初めてなんですが、Udemyの講座で軽めにインプットしたのが1ヶ月以上前だったので完全に忘れてしまって思い出すためにUdemyの動画を見たり、社内LTでの登壇が2回あってその準備をしていたりしてましたね(言い訳)。

参考:Reactの勉強に使ったUdemyの講座たち

React×Reduxの基礎はこちらで
フロントエンドエンジニアのための React ・ Redux アプリケーション開発入門

Hooksの基礎はこちらで
React Hooks 入門 - Hooksと Redux を組み合わせて最新のフロントエンド状態管理手法を習得しよう!

あ、あと会社でReactのチュートリアルをもらったのでそれを苦しみながら進めたりもしていましたね。React×Redux自体慣れてないところにTypeScriptとテストコードも組み込まれていて、途中から何が分からないのかが分からないくらいに難しかったので挫折してしましました。このアプリを作ってレベルアップしてから再挑戦する。

DB設計

だいたい似たようなアプリケーションは数回作ったことあるので、過去の自作アプリを参考にサクっと設計していきます。

ちゃんとやるんだったらER図を作ったりしながらやるのがいいんでしょうが、面倒臭がりなのでスプレッドシートでやっちゃいました。
(ER図を作れるサイト:dbdiagram.io

こんな感じ↓↓↓
スクリーンショット 2020-10-04 21.42.42.png

softdeleteってのはカラム名ではなくLaravelのソフトデリートという「論理削除」機能のことを指していて、削除メソッドを実行してもDBにレコードは残ったままで、代わりにdeleted_atカラムに削除時間が記録されます。deleted_atに値が入っているものは、データ一覧を取得した際も含まれないので便利です。

Laravel ソフトデリート

環境構築

Laravelに関しては必要最低限な分だけやり、フロントに関しても以前のプロジェクトのものをまるっと持ってくることで時間節約します。
個人的には、browser-syncだけ動いてくれればそれでいい。保存したら自動でブラウザを更新、それだけで十分です。

ちなみにLaravelのバージョンは v 7.26.1でインストール当時の最新版です。
(7系は記事がまだ少ないので、6系にしとけばよかったと思いました。。)

package.json
{
    "private": true,
    "scripts": {
        "dev": "npm run development",
        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "watch": "npm run development -- --watch",
        "watch-poll": "npm run watch -- --watch-poll",
        "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --disable-host-check --config=node_modules/laravel-mix/setup/webpack.config.js",
        "prod": "npm run production",
        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
    },
    "devDependencies": {
        "@babel/preset-react": "^7.0.0",
        "axios": "^0.19",
        "bootstrap": "^4.0.0",
        "browser-sync": "^2.25.0",
        "browser-sync-webpack-plugin": "^2.0.1",
        "cross-env": "^7.0",
        "import-glob-loader": "^1.1.0",
        "jquery": "^3.2",
        "laravel-mix": "^5.0.1",
        "lodash": "^4.17.19",
        "popper.js": "^1.12",
        "react": "^16.2.0",
        "react-dom": "^16.2.0",
        "react-redux": "^7.2.1",
        "react-router-dom": "^5.2.0",
        "redux": "^4.0.5",
        "resolve-url-loader": "^3.1.0",
        "sass": "^1.15.2",
        "sass-loader": "^8.0.0",
        "styled-components": "^5.2.0"
    }
}
webpack.mix.js
const mix = require('laravel-mix');

mix.webpackConfig({
    module: {
        rules: [{
            test: /\.scss/,
            enforce: 'pre',
            loader: 'import-glob-loader'
        }]
    }
})
/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.react('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css')
    .sourceMaps()
    .browserSync({ 
        https: false, // httpsのサイトをproxyするならtrueをセット
        files: [ 
            './resources/**/*',
            './app/**/*',
            './config/**/*',
            './routes/**/*',
            './public/**/*'
        ],
        proxy: {
            target: 'http://127.0.0.1:8000'
        },
        open: true, //BrowserSync起動時にブラウザを開かない
        reloadOnRestart: true //BrowserSync起動時にブラウザにリロード命令おくる
    });

フロントのnpmモジュールはnodeのバージョン依存によるエラーが頻繁に起こって超厄介なのですが、browser-syncも例外なくそれにあたります。
以前のプロジェクトで、
node: v 9.11.0
browser-sync: v 2.25.0
という組み合わせでうまくいったので、今回もbrowser-syncはv2.25.0を指定してインストール。ちなみに今回nodeはv 12.11.0ですが、これでうまくいきました。

あ、あとESLintとStyleLintは入れたいところだな。。こいつらも設定がバッティングしてしまってうまく動かすのが難しいんですよね。。

いざ実装

SPAってなんぞ?

SPAとは、Single Page Application(シングルページアプリケーション)の略称であり、単一のWebページでアプリケーションを構成する設計構造の名称です。

SPAの説明については別リンク参照しておきますが、ユーザー目線でいうと「動作がサクサクになって快適!」ってことで是非とも採用したいと考えました。

デメリットの一つとして実装コストがかかるとのことですが、気合いで乗り切りましょう(無策

(参考)SPAについて
SPA(Single Page Application)ってなに?
SPA(Single Page Application)の基本

認証系をSPAで書き換える

以前の記事を書いてから今回までの間、ほとんどの時間をこの「認証系のSPAへの書き換え」に費やしていました。
Laravelはコマンド一つで認証系が構築できてしまうのですが、SPAをやろうとするといろいろとLaravel側の設定を書き換えなければいけなくて、これがまあ難しかった。。。

セッション認証とトークン認証

認証系を理解するうえで、この違いを認識するところからのスタートでした。(正直いまだにあんまり理解できてない)
フォームから値を送信してユーザー登録やログインを行うのはセッション認証です。
一方トークン認証は、ログイン情報をサーバー側に保持しないという点でセッション認証と違います。

(参考)セッション認証とトークン認証についてはこちら
Cookie(Session)での認証と Token での認証の違いについて
JWT・Cookieそれぞれの認証方式のメリデメ比較

Laravelの「Sanctum」という認証システム

そして、LaravelにはSPA認証をお手軽に実装できる「Sanctum」というライブラリが存在することを知ったので、こいつを使うことにしたのですが、ここで1つ大きな勘違いをしていました。

(参考)
Laravel 7.x Laravel Sanctum

何となくいろんなところで「SPAでwebアプリを作る場合、バックエンドをAPIとして使う」という情報を見聞きしていたので、「SPAならAPIかー」という浅い理解をしていました。

で、「SPA API 認証」というキーワードでぐぐると、「SPAではトークン認証をしましょう」という記事がヒットします。当然僕は、「そうか!SPAはトークン認証なんだ!」と思い、先ほどのSanctumを使ったトークン認証を実装していました。

↓これ
スクリーンショット 2020-10-06 6.12.06.png

トークン認証に書き換えるの、めっちゃ苦労しました。。。
(たぶん10時間くらいはかかりました。HTTPリクエストヘッダーの「Authorization」にBearerトークンをセットしなきゃいけないとか、まじで ?????? でした)

そしてもがき苦しみながらトークン認証に書き換え終わったころ、見てはいけないものを見てしまいました。

SanctumでSPAを認証するのにAPIトークンを使ってはいけません

先ほどのSanctumのAPIトークン認証のページ

スクリーンショット 2020-10-06 6.12.06.png

皆さん自身のファーストパーティSPAを認証するためにAPIトークンを決して利用してはいけません。代わりに、Sanctumの組み込みSPA認証を使用してください。

、、、トークン認証ダメ!ゼッタイ!ってめっちゃ書いてあるやんけ!!!

スクリーンショット 2020-10-06 6.24.29.png

うん、APIトークン認証のちょっと下にSPA認証の項ががっつり書いてあった。。

よくよく調べてみると、「Laravel×SanctumでSPA認証」みたいな記事をみると、ちゃんとこのSPA認証の内容が載ってる。。

(参考)Laravel×Sanctum によるSPA認証の記事

一番わかりやすかった記事
Laravel 7.x Sanctumの使い方!実例

SanctumじゃないしVueの記事だけど、SPAでアプリを作るチュートリアルでLaravel側の設定などはかなり参考になる
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (4) 認証API

どうやら、SPAのように「フロントとバックの分離」をするときはトークン認証をするのが一般的?なようですが、SanctumではLaravelに組み込まれているセッション認証を使うようです。セッション認証だとCSRF保護が使えたりするので、トークン認証よりセキュリティ面で安心なようです(説明間違ってたらごめんなさい。。)

書き換えたトークン認証の処理を戻す

というわけで、頑張って書き換えたトークン認証の処理は必要ないということが分かりました。
セッション認証ならそんなに難しくないので、さくっとトークン認証から書き換えて一件落着。

まとめ

今回の個人開発、技術的な側面でいうと「Reactのアウトプット」がメインだったはずなのに、今のところほぼLaravelしか触ってない。サーバーサイドは使い慣れたLaravelで学習コストを抑えよう〜なんて思ってたけど、とんでもなかった。SPAの認証って難しい。

かなりもがき苦しみましたが、認証系をSPAに書き換える過程でHTTP通信についてもかなり勉強して理解が深まった気がします。自分でHTTPリクエストのヘッダーを書き換えたり追加したりなんてしたことなかった。

みんなのポートフォリオまとめサイト、いつになったら完成できるのか。
最初は9月いっぱいで作るとかふざけたこと言ってたような気がします。10月ですら無理そうなので、11月いっぱいを目標に変更しよう。

亀の歩みですが、認証系が一通り終わったのでSPA実装の最初の「0→1」フェーズは超えたと思ってます。
ここからは一気に加速して、、、いく、、、、はず

おわりに

Qiitaは不定期更新でやっておりますが、Twitter ( @kiwatchi1991 ) では日々感じたことや細かい試行錯誤の様子などを発信しております。

また、ブログを毎日更新していて(今は50日を超えたあたり)、数日に一度個人開発に関する記事を書いています。

38日目 個人開発記⑤-実装開始-
39日目 個人開発記⑥-思い込み-
44日目 個人開発記⑦-結局Laravel-

各論はそっちで細かく書いてたりもするので、よかったらこちらもぜひのぞいてみてください。

それではまた次回!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsにて環境変数を用いて、APIのURLを変更する方法

概要

SPA構成のシステムを開発する際に、本番環境や開発環境によってAPIのURLが変わるので、それを環境ごとに切り替えたい!ってことよくありますよね。
こういう場合は環境変数を用いて使用するURLを切り替えるのがオーソドックスなのですが、Next.jsだと環境変数の取り扱いが少し変わってくるので、それを今回まとめてみました!

やりたいこと

以下の3種類の環境のAPIを用意しています。

1. 本番環境用のAPI (production)
2. 開発環境用のAPI (development)
3. 開発環境用のモックAPI (swagger)

3つ目のモックAPIはOpenAPI(swagger)と呼ばれるもので、スキーマを用いてAPIを定義するだけで擬似的にAPIを使用することができます。
サーバーサイドのAPIの開発を待ってからフロントのAPI関連の実装をすると効率が悪いので、swaggerを用いてモックAPIを作成し、先行してフロントの開発を実行することができます。

Open APIについては以下の記事が分かりやすいのでご覧いただければと思います。

よって、上記3つのAPIのURLを環境ごとに切り替えていく必要があります。

Nuxt.jsの場合

Next.jsの方法の前にオーソドックスのやり方として、まずNuxt.jsにおける環境ごとの切り替え方法を説明します。

まずは以下の3つのenvファイルにそれぞれの環境のAPIのURLを記載します。

env.production.js
module.exports = {
  // APIのURL
  apiBaseUrl: 'https://prodaution.example.com'
}
env.development.js
module.exports = {
  // APIのURL
  apiBaseUrl: 'http://localhost:80'
}
env.swagger.js
module.exports = {
  // APIのURL
  apiBaseUrl: 'http://localhost:4010'
}

そしてこのenvファイルをnuxt.config.jsにて呼び出して、「env」に設定します。
これによってNuxtプロジェクト全体で「process.env.apiBaseUrl」の記載でAPIのURLを呼び出すことができます。
取得したAPIのURLをaxiosに設定することでAPI通信ができるようになるので結構便利です!

nuxt.config.js
// 1. NODE_ENVから環境変数を呼び出す
const environment = process.env.NODE_ENV || development
// 2. envファイルを呼び出す
const envSet = require(`./env.${environment}.js`)


export default {
  ・・・
  // 3. envファイルを設定
  env: envSet,

}

「NODE_ENV」から値を取得していますが、これはどこから取得しているのでしょう?
これは「yarn dev」や「yarn start」などでNuxtを実行した際に取得しています。

package.jsonに以下のように記載します。

package.json
"script": {
  "build": "NODE_ENV=production nuxt build", // 本番環境
  "dev" : "NODE_ENV=development nuxt", // 開発環境
  "dev:swagger" : "NODE_ENV=swagger nuxt" // 開発環境 モックAPI
}

この設定によって「yarn dev」コマンドを実行した際に、"NODE_ENV=development nuxt"が実行されるので、NODE_ENVに"development"が与えられ、nuxt.confing.jsで読み込まれるという仕組みです。

Open API(swagger)のモックAPIを使用したい場合は「yarn dev:swagger」のコマンドを実行し、NODE_ENVから"swagger"を取得して、nuxt.config.jsonの「env」に「env.swagger.js」の値を設定するようにします。

これでコマンドだけで簡単に各環境のAPIを使用できるようになりました。

Next.jsの場合

それではNext.jsの場合も同様にやってみましょう!
まずNuxt.jsと同じようにenvファイルを作成しましょう。
Next.jsの場合は「.js」のような拡張子はいらず、「.env.production」のようなファイル名にします。

 .env.production
// 本番環境APIのURL
API_BASE_URL=https://prodaution.example.com
 .env.development
// 開発環境APIのURL
API_BASE_URL=http://localhost:80
 .env.swagger
// モックAPIのURL
API_BASE_URL=http://localhost:4010

Next.jsにおけるNODE_ENVの取り扱い

ここがNext.jsで環境変数を使用する際のハマりポイントです。。
Nuxt.jsとは異なり、コマンドにNODE_ENVを仕込むことは非推奨とされています。

試しに"NODE_ENV=swagger next"のようなコマンドを作成し実行すると、consoleに以下のようなwarningが表示されます。

You are using a non-standard "NODE_ENV" value in your environment. This creates inconsistencies in the project and is strongly advised against. Read more: https://err.sh/next.js/non-standard-node-env

またこちらの記事でも記載の通り、Next.jsはコマンドごとにNODE_ENVに設定される値が決められています。

// NODE_ENVの値
- 「next dev」コマンドでは、developmentの値が設定される
- 「next build」、「next start」コマンドでは、productionの値が設定される
- 「jest」実行時は、testの値が設定される

また、NODE_ENVの値によって、適用される「.env」ファイルも自動で適用されます。

- developmentの場合、「.env.development」または「.env.development.local」
- productionの場合、「.env.production」または「.env.production.local」
- testの場合、「.env.test」または「.env.test」

なので、Next.jsではNuxt.jsのように実行コマンドにNODE_ENVを仕込んで切り替えることができません。

Next.jsにてAPIのURLを変更する方法

ではどうするか?ですが、結論をいうと、「NODE_ENV」以外の環境変数を使用すればいい!です!
(結構無理やりなやり方ですが。。)

参考

そのためには「dotenv」というライブラリが必要になるのでinstallしておきます。

yarn add dotenv

そしてpackage.jsonのscriptコマンドを以下のように記載します。

package.json
"scripts": {
    "dev": "next",
    "dev:swagger": "ENVIRONMENT=swagger next",
    "build": "next build",
    "start": "next start",
  },

モックAPIを呼び出すコマンド「yarn dev:swagger」に、"ENVIRONMENT=swagger next"を実行しています。
環境変数に"ENVIRONMENT"を使用しているので、これで「process.env.ENVIRONMENT」でコマンドに設定した値を取得することができます。

次にnext.config.jsにenvファイルを取得する設定を記載します。

next.config.js
// 各環境のenvファイルを読み込む
require('dotenv').config({ path: `./.env.${process.env.ENVIRONMENT}` })

module.exports = {}

注意点その1

しかし、これでenvファイルの設定値が使用できるようになったわけではありません。

Next.jsではクライアント側で使用したい環境変数には、頭文字に「NEXT_PUBLIC_」をつける必要があります。
これを付けないと、サーバー側でしか使用することができなくなります。

よって各envファイルの環境変数を以下のように変更します。

 .env.production
// 本番環境APIのURL
NEXT_PUBLIC_API_BASE_URL=https://prodaution.example.com
 .env.development
// 開発環境APIのURL
NEXT_PUBLIC_API_BASE_URL=http://localhost:80
 .env.swagger
// モックAPIのURL
NEXT_PUBLIC_API_BASE_URL=http://localhost:4010

これでクライアント側で「process.env.NEXT_PUBLIC_API_BASE_URL」でAPIの値を取得し使用することができます。

※クライアント側で使用できるということは開発者ツールなどでその値を外部に公開する事になります。機密事項など公開しないように注意してください。

注意点その2

これでAPIのURLを使えるようになったぞ!と思いますが、最後に1つ落とし穴があります。

それは「.env.development」と「.env.swagger」で使用されているAPIの環境変数が同じ変数名であるため、モックAPIのコマンドを実行しても、「.env.development」のAPIのurlが反映されてしまう事です。

なぜこのようなことが起こるのか?
それは実行コマンドを確認すれば理解できます。

実行コマンドは「"dev:swagger": "ENVIRONMENT=swagger next",」となっています。

この実行順は、以下のようになります。

1. 「ENVIRONMENT=swagger」でnext.config.jsにて「.env.swagger」を読み込む
2. 「next」で自動的に「.env.development」が適用される

つまり、1度「.env.swagger」で読み込まれてから「.env.development」を読み込むため、同じ変数名のものは 「.env.developent」に記載のものに上書かれてしまいます。

ではどうすればいいか?

それは「違う変数名を使用する」です!(これも結構無理やり。。)

「.env.swagger」の環境変数を以下のように変更します。

 .env.swagger
// モックAPIのURL
- NEXT_PUBLIC_API_BASE_URL=http://localhost:4010
+ NEXT_PUBLIC_OPEN_API_BASE_URL=http://localhost:4010
+ NEXT_PUBLIC_ENVIRONMENT=swagger

こうすることで「.env.development」の値で上書きされないようにします。

また新たに「NEXT_PUBLIC_ENVIRONMENT」を追記しています。
これはクライアント側で「NEXT_PUBLIC_OPEN_API_BASE_URL」(モックAPI)を使うか、「NEXT_PUBLIC_API_BASE_URL」(開発環境、本番環境のAPI)を使うかを判断するために使用します。

使用方法は以下のようになります。

xxx.js
/**
 * API関数取得メソッド
 */
const resolveApiBasePath = () => {
  /// 環境変数「NEXT_PUBLIC_OPEN_API_BASE_URL」が存在する場合、その値を取得
  const env = process.env.NEXT_PUBLIC_ENVIRONMENT
    ? process.env.NEXT_PUBLIC_ENVIRONMENT
    : ''

  switch (env) {
    case 'swagger':
      // "swagger"の場合、swaggerのAPIのURLを使用する
      return process.env.NEXT_PUBLIC_OPEN_API_BASE_URL
    default:
      // development, productionのAPIのURLを使用する
      return process.env.NEXT_PUBLIC_API_BASE_URL
  }
}

これでswaggerの時はモックAPIのURLを適用させ、development, productionの場合はそれぞれのAPIのURLを適用させることができます。

※「.env.development」にswaggerと同じ名前の環境変数を記載すると上書かれるので、それぞれ一意の変数名を定義してください。

最後に

少し長くなりましたが、以上がNext.jsにて環境変数を用いてAPIのURLを変更する方法の説明になります。

最終盤のソースコードをまとめますので、ご活用いただけると幸いです。

envファイル

 .env.production
// 本番環境APIのURL
NEXT_PUBLIC_API_BASE_URL=https://prodaution.example.com
 .env.development
// 開発環境APIのURL
NEXT_PUBLIC_API_BASE_URL=http://localhost:80
 .env.swagger
// モックAPIのURL
NEXT_PUBLIC_OPEN_API_BASE_URL=http://localhost:4010
NEXT_PUBLIC_ENVIRONMENT=swagger

package.json

package.json
"scripts": {
    "dev": "next",
    "dev:swagger": "ENVIRONMENT=swagger next",
    "build": "next build",
    "start": "next start",
  },

next.config.js

next.config.js
// 各環境のenvファイルを読み込む
require('dotenv').config({ path: `./.env.${process.env.ENVIRONMENT}` })

module.exports = {}

クライアント側でのAPIURLの取得

xxx.js
/**
 * API関数取得メソッド
 */
const resolveApiBasePath = () => {
  /// 環境変数「NEXT_PUBLIC_OPEN_API_BASE_URL」が存在する場合、その値を取得
  const env = process.env.NEXT_PUBLIC_ENVIRONMENT
    ? process.env.NEXT_PUBLIC_ENVIRONMENT
    : ''


  switch (env) {
    case 'swagger':
      // "swagger"の場合、swaggerのAPIのURLを使用する
      return process.env.NEXT_PUBLIC_OPEN_API_BASE_URL
    default:
      // development, productionのAPIのURLを使用する
      return process.env.NEXT_PUBLIC_API_BASE_URL
  }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む