20200724のReactに関する記事は10件です。

React + IndexedDBでCRUD作成、Form登録など #React #React.js #node

概要

React ,node.js版で
IndexedDBのCRUD作成となります
dexieライブラリで、IndexedDB操作して。
ブラウザ内に保存する形で。使用を想定しています

環境

react
react-dom
react-router-dom
dexie

画面

・リスト

ss-crud-0724a.png

・編集
ss-crud-edit-0724b.png

参考のコード

https://github.com/kuc-arc-f/react_cms1_1crud


実装など、React Component

・Create
https://github.com/kuc-arc-f/react_cms1_1crud/blob/master/src/component/Task/Create.js

・index
https://github.com/kuc-arc-f/react_cms1_1crud/blob/master/src/component/Task/Index.js

・edit/delete
https://github.com/kuc-arc-f/react_cms1_1crud/blob/master/src/component/Task/Edit.js


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

ReactとNodeでsocket

Node.js

% npm i socket.io
  server = app.listen(port);
  var socket = require('socket.io');
    io = socket(server); // ①
    io.on('connection', (socket) => { // ②
      const usr = Object.keys(io.sockets.sockets).length
      console.log("Hi Server")
      console.log(usr)
      socket.on('SEND_MESSAGE', function(data){ // ③
        io.emit('RECEIVE_MESSAGE', data); // ④
      })
    });

React

% sudo yarn add socket.io socket.io-client 
  import socketIOClient from 'socket.io-client'
    const socket = socketIOClient(`${API_URL}`,{transports: ['websocket']});
    socket.on('connection', (socket) => { 
      console.log("Hi client");
      console.log(socket);
    });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Redux】基礎文言

最近Reduxについて勉強を始めたのですが、難しくて、初見では頭にスパッと入らず苦労しています。。。
なので今回は、技術的な投稿ではなく、基礎文言を復習していきたいと思います。

(ただのアウトプットなので、参考にはなりません。)

Actionとは

Actionとは、
・JavaScirptのオブジェクトで、オブジェクトの内部で type というキーと、それに対応する値を持つ。
・Applicationの中で何が起きたかを示すデータのこと。

特徴として、
type はユニークなものではいけないという特徴がある。

ActionCreater

ActionCreaterとは、
Actionを返す関数のことです。

Actionで定義したオブジェクトの記述のみでは、アプリケーションで活用することができません。
なので、Actionを返す関数を定義する必要があります。

Reducer

Reducerとは、
Action が発生した時に、そのActionに組み込まれている type に応じて、状態をどう変化させるかを定義するものです。

connect

connectとは、
connect関数を使用して、state や action と Component とを関連付けて、ビューのイベントで状態を遷移させて、遷移後の状態を画面に再描画する。

mapStateToPropsとは、
state の情報から Component に必要なものを取り出して、 Component 内の props としてマッピングする機能を持つ関数です。引数には状態のトップレベルを持つ state を書いてどういったオブジェクトを props として対応させるのかを関数の戻り値として定義します。

mapStateToPropsとは、
ある action が発生した時に、 reducer に type に応じた状態繊維を実行させるための関数が dispatch になります。

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

Jamstack構成メディアのプレビュー機能を、サーバーレスAPIを使って実装した

社内で運用するオウンドメディア開発にあたり、Jamstack構成でのプレビュー機能の開発で得た知見などをまとめておきます。構成はNext.js + Contentfulで、ホスティングサービスはAmplify Consoleを使っていますが、上記スタックに特有の機能を使ったとかではないので、他の構成でも使えるかと思います。

最初にざっくりまとめ

  • プレビューデータを取得するためのAPIをサーバレス構成で作成した
  • APIはAPI Gateway + Lambdaで作成し、リソース構築にはServerless Frameworkを利用した
  • APIの認可処理にはCognitoを利用した

Jamstackでのプレビュー機能の設計検討

ブログやオウンドメディアをJamstack構成で構築する際、基本的に各ページは事前ビルドしておき静的ページとして配信するSSGをベースに開発することになると思います。

ただ、執筆中の記事を確認する機能であるプレビュー機能については、SSGでは不可能ではないもののいまいち使い勝手が悪いものになりがちです。プレビューを確認するたびにアプリのビルドを待たなければならず、装飾などを確認したいだけなのに数分待たされるのはユーザーからすると体験は最悪です。いくら社内メンバーしか使わないといっても、不満が噴出すること間違いなしです。

SSRを使って解決する?

解決策の一つとして、プレビューページについてはSSRやクライアント側からリクエストで動的に内容を取得する方法があります。
Next.jsを使うならば、Vercelにデプロイするのが一番簡単にSSRを利用できて、手間もかからない方法です。

ただ、どんなケースでもVercelを使えるというわけではなく、弊社のケースではインフラ構成は基本的にAWSに寄せているため、今回構築するオウンドメディアについてもAmplify Consoleに乗せたいという要件がありました。

現時点ではAmplify Consoleは公式にSSRの利用をサポートしておらず(開発はしているようです: https://github.com/aws-amplify/amplify-js/issues/5435 )、うまいことやれば似たようなことはやれるのかもしれませんが、運用面を考えるとあまりハック的なことはやりたくありません。

上記の理由から、今回はSSRは採用しない方針となりました。

クライアント側から直接Contentfulなどを叩く?

クライアント側からプレビューデータを取得する方法では、ContentfulやmicroCMSのアクセスキーがブラウザに渡ってしまうという問題があります。Contentfulの場合はDelivery keyとManagement keyが分かれていて、Delivery keyが漏洩してもデータが変更されたり、破壊されたりという被害は起きないはずですが、たとえば大量のリクエストを送信してrate limitに到達させるといった悪用方法は考えられます。

Next.jsはデフォルトでバンドルファイルのチャンクをサポートしてくれるので、該当するファイルが別のファイル内から参照されない限りはチャンク後のファイルはダウンロードしないで済みます。そのため、プレビューページへのリンクをどこからも貼らないようにすれば、ブラウザにアクセスキーが渡ってしまうという問題は回避できます。

とはいえ、こういったリポジトリ特有の制約はできれば作りたくないですし、サーバーにjsファイル自体は配置されるため、URLを直接叩かれた場合は結局アクセキーの漏洩につながってしまいます(可能性自体は低いとはいえ)

上記のような懸念点が生まれるのはそもそもクライアント側だけで問題を解決しようとしているためで、素直にプレビューデータを返却するAPIを作れば万事解決するのではないかということで、今回の開発においてはプレビューデータを取得する用のエンドポイントを作成することにしました。

プレビューAPI利用におけるユーザー認証について

プレビューデータは基本的に公開してはいけないものであるため、社内の人間以外からは見えないようにする必要があります。そこでエンドポイントには認可処理を挟むこととし、それに失敗した場合は401を返却するような設計としました。

技術構成の検討

ここまでの検討で、プレビューAPIに求められる要件は以下のようなものです。

  • プレビューデータを返すエンドポイントを一つだけ実装すれば十分
  • ユーザー認証機能、およびエンドポイントでの認可処理が必要

Djangoなどを使って自前でサーバーを実装するのが素直な方法かもしれませんが、プレビュー機能のためだけに認証機能を含むサーバーを開発し、加えてインフラを整えるのも面倒です。

そこでエンドポイントについてはAPI Gateway + Lambdaで実装することとし、認証、認可機能についてはCognitoを使うことにしました。API GatewayとCognitoは簡単に連携できるのも嬉しいポイントです。

プレビューデータ取得の処理を簡単なフローチャートにすると以下のような図になります。

スクリーンショット 2020-07-24 14.54.37.png

また、これらのAWSリソース構築についてはServerless Frameworkを使うのがいいよというアドバイスをもらったので、リソース構築、管理についてはServerless Frameworkを採用することとしました。

Serverless FrameworkによるAWSリソース構築

今回の開発における必要なAWSリソースはAPI Gateway、Lambda、Cognitoの3つです。CognitoはユーザープールとIDプールがあり、「Serverless Framework Cognito」などと検索するとIDプールまで含めた構築方法の説明が多く出てきます。

ただ、今回のユースケースではユーザーの権限によって認可の範囲が異なるわけではなく、認証されているかどうかのtrue or falseの判定で十分なため、ユーザープールの作成のみで十分です。

serverless.yml

上記スタックをまとめて構築するymlファイルは以下のようなものになります。

serverless.yml
service: sample-serverless

plugins:
  - serverless-domain-manager

custom:
  stage: ${opt:stage, self:provider.stage}

  domains:
    dev: dev-preview.sample.jp
    prod: prod-preview.sample.jp

  customDomain:
    domainName: ${self:custom.domains.${self:custom.stage}}
    stage: ${self:custom.stage}
    certificateName: sample.jp
    createRoute53Record: true

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  region: ap-northeast-1
  endpointType: REGIONAL
  iamRoleStatements:
    - Effect: Allow
      Action:
        - cognito-idp:ListUsers
        - cognito-idp:AdminListGroupsForUser
      Resource: "arn:aws:cognito-idp:*"

functions:
  preview:
    handler: handler.preview
    name: ${self:service}-${self:custom.stage}
    events:
      - http:
          path: /
          method: get
          cors: true
          integration: lambda
          authorizer:
            name: ${self:custom.stage}-sample-authorizer
            type: COGNITO_USER_POOLS
            arn:
              Fn::GetAtt: [CognitoUserPool, Arn]
          request:
            template:
              application/json: '{ "slug" : "$input.params(''slug'')" }'

resources:
  - ${file(./resources/cognito-user-pool.yml)}
  - ${file(./resources/authorizer.yml)}

いくつかの箇所について解説します。

plugins:
  - serverless-domain-manager

上記の例ではAWSリソース構築とともに独自ドメインでの公開 + SSL化を行っています。それに必要なのがserverless-domain-managerで、デプロイ実行前にnpm i --save devしておけばOKです。
具体的な使い方は公式ドキュメントを参考にしてください。

functions:
  preview:
    handler: handler.preview
    name: ${self:service}-${self:custom.stage}
    events:
      - http:
          path: /
          method: get
          cors: true
          integration: lambda
          authorizer:
            name: ${self:custom.stage}-sample-authorizer
            type: COGNITO_USER_POOLS
            arn:
              Fn::GetAtt: [CognitoUserPool, Arn]
          request:
            template:
              application/json: '{ "slug" : "$input.params(''slug'')" }'

API Gateway、およびLambdaの構築に関する記述です。authorizerには同時に生成されるCognito User PoolのArnを指定しています。これだけでエンドポイントに認可機能が設定されます。めちゃくちゃ便利ですね。

また、プレビューデータを取得する際、クエリパラメータに記事のスラッグをつけてリクエストする設計にしています。Lambda関数内でそのクエリパラメータを取得できるようにするため、request以下の記述をしています。

resourceの箇所に記述しているcognito-user-pool.ymlauthorizer.ymlは以下のようになっています。

cognito-user-pool.yml
Resources:
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      AutoVerifiedAttributes:
        - email
      MfaConfiguration: "OFF"
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: false
          RequireUppercase: true
      UserPoolName: ${self:custom.stage}-sample-userpool
      UsernameAttributes:
        - email

  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: ${self:custom.stage}-sample-client
      ExplicitAuthFlows:
        - ALLOW_USER_PASSWORD_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH
      GenerateSecret: false
      UserPoolId:
        Ref: CognitoUserPool

CognitoUserPoolではパスワードの条件や各ユーザーが持つパラメータ(メールアドレスや名前など)を指定しています。今回はそれらは重要ではないので、メールアドレスだけ持っておけば十分かなと思います。

CognitoUserPoolClientでは認証手段についての設定を記述しています。重要なのはExplicitAuthFlowsで、いくつかの認証フローがあります。このへんは公式ドキュメントを見るのが早いです。

authorizer.yml
Resources:
  GatewayResponseDefault4XX:
    Type: 'AWS::ApiGateway::GatewayResponse'
    Properties:
      ResponseParameters:
        gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
        gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
      ResponseType: DEFAULT_4XX
      RestApiId:
        Ref: 'ApiGatewayRestApi

API Gatewayのオーソライザーに関する記述です。ハマったのがGatewayResponseDefault4XXの箇所で、これを記述しておかないとオーソライザーを設定した際に必ずCORSのエラーレスポンスが帰ってきてしまうため、忘れずに設定しておく必要があります。

handler.js

Lambdaにデプロイする関数を記述するファイルです。今回の例ではこんな感じになります。

handler.js
"use strict";

const axios = require("axios");

module.exports.preview = async (event, context, callback) => {
  const headers = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Credentials": true,
    "Access-Control-Allow-Headers":
      "Origin, X-Requested-With, Content-Type, Accept",
  };

  const path = `https://preview.contentful.com/spaces/${process.env.CONTENTFUL_SPACE_ID}/environments/${process.env.CONTENTFUL_ENVIRONMENT}/entries/?access_token=${process.env.CONTENTFUL_PREVIEW_KEY}&content_type=xxx%&fields.slug=${event.slug}`;

  try {
    const res = await axios.get(path);

    if (!res.data.items[0]) {
      callback(JSON.stringify({
        status: "[404]",
        message: "Not Found"
      }))
    }

    const image = res.data.items[0].fields.image
      ? res.data.includes.Asset[0].fields
      : null;

    return {
      status: 200,
      headers: headers,
      body: JSON.stringify({
        data: res.data.items[0].fields,
        image: image,
      }),
    };
  } catch (err) {
    callback(JSON.stringify({
      status: "[500]",
      message: "Internal Server Error"
    }))
  }
};

とくにややこしいことはしておらず、Contentfulのプレビューデータを取得するAPIを叩いて、帰ってきたデータを多少フォーマットして返却するだけの関数となっています。

一つ気をつける必要があるのは、エラーレスポンスの返却方法です。たとえば、以下のように記述してもクライアントには200 OKが返ってしまいます。

return {
  status: 500,
  headers: headers,
  message: "Internal Server Error"
};

これはAPI Gatewayを経由する際に、Lambdaからのレスポンスがエラーレスポンスとして認識されないため、API Gatewayが200 OKを返してしまうためです。

API Gatewayにはレスポンス内容をチェックしてレスポンスステータスを決定する仕組みがあるので、それを使ってエラーレスポンスが返るようにします。上記の例で

callback(JSON.stringify({
  status: "[500]",
  message: "Internal Server Error"
}))

status: "[500]"のようにしているのは、API Gatewayのデフォルト設定がそのようになっているためです。status: 500のようにしたい場合は、API Gateway側の設定を変更する必要があります。

デプロイの実行

serverless.ymlとhandler.jsを準備できたら、sls deploy -vと入力するだけでstageがdevの各リソースが構築されます。

package.jsonのスクリプトに

"scripts": {
  "deploy:dev": "sls deploy -v --stage dev",
  "deploy:prod": "sls deploy -v --stage prod"
}

などと記述しておけばより便利です。

クライアント側での認証機能の実装

Cognitoでの認証処理の実装については、公式がSDKを配布しているのでそれを使います(amazon-cognito-identity-js)。

各目的ごとのサンプルコードも記載されており、ほぼそのまま使うだけで認証処理ができるので便利です。今回のケースで言えば、社内の限られた人のみが使うというものなのでサインアップ機能は必要なく、ログイン機能だけあれば十分です(あらかじめユーザーは作成しておく)。

ログイン処理のサンプルコードは以下のようなものです。

Cognito.ts
import {
  CognitoUserPool,
  CognitoUser,
  AuthenticationDetails,
} from "amazon-cognito-identity-js";

const poolData = {
  UserPoolId: process.env.COGNITO_USER_POOL_ID,
  ClientId: process.env.COGNITO_CLIENT_ID,
};

const userPool = new CognitoUserPool(poolData);

export const login = async (
  email: string,
  password: string,
  newPassword: string
) => {
  if (!password) {
    return Promise.reject();
  }

  const authenticationDetails = new AuthenticationDetails({
    Username: email,
    Password: password,
  });

  const userData = {
    Username: email,
    Pool: userPool,
  };
  const cognitoUser = new CognitoUser(userData);

  cognitoUser.setAuthenticationFlowType("USER_PASSWORD_AUTH");
  return new Promise((resolve, reject) => {
    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: () => {
        resolve();
      },
      onFailure: () => {
        reject();
      },

      // 初回ログイン時のみ呼ばれるcallback
      newPasswordRequired: (userAttributes) => {
        delete userAttributes.email_verified;
        cognitoUser.completeNewPasswordChallenge(newPassword, userAttributes, {
          onSuccess: () => {
            resolve();
          },
          onFailure: () => {
            reject();
          },
        });
      },
    });
  });
};

amazon-cognito-identity-jsのようにやや巨大なライブラリを扱う場合はラッパー関数を用意する方法が便利です。テスト時のモックも簡単に実装できるようになります。

ほぼ公式ドキュメントのサンプルコードそのままなのですが、cognitoUser.authenticateUserをPromiseでラップして、関数に戻り値を設定しています。

もともとcognitoUser.authenticateUserは内部で引数として渡したcallback関数を実行する仕様で戻り値がvoidなのですが、戻り値を設定しておくことで関数外部でログイン成功、失敗時の処理を分岐させることができます。イメージはこんな感じです。

try {
  await cognitoLogin(email, password, newPassword);
  setResultMessage(
        "ログインに成功しました。"
  );
} catch (err) {
  setResultMessage("ログインに失敗しました。");
}

また、cognitoUser.authenticateUserのcallbackにnewPasswordRequiredという関数を置いていますが、これはCognitoで新規作成したユーザーの有効化のためです。新規作成したユーザーは一度パスワード変更を行わないと有効化されず、初回ログイン処理の場合はこちらのnewPasswordRequiredが呼ばれるようになっています。

認証トークンの取得

上記のSDKを利用すると、ログインに成功した場合に自動的にlocalStorageに認証トークンが保存されます。それを取得するための関数も用意されています。

export const getAccessToken = (): string => {
  let token = "";
  const currentUser = userPool.getCurrentUser();
  if (currentUser) {
    currentUser.getSession((err: unknown, result: CognitoUserSession) => {
    if (result) {
      token = result.getIdToken().getJwtToken()
    }
  }

  return token;
};

こちらもラッパー関数を作成しました。やっていることは単純で、ログイン状態かどうかを判定し、ログイン状態であればlocalStorageからトークンを取得して返却しているだけです。

あとはAPIへのリクエスト時にヘッダに上記トークンを付加して送信すればOKです。

まとめ

プレビュー機能を作るためだけに上記のような構成を考えたりするのはなかなか大変でしたが、運用面での課題もとくになさそうで、そこそこ綺麗に設計、実装できたように感じています。

Jamstack構成はカスタマイズ性が高く、フロントエンドエンジニアにとっては魅力的ですが、多くの機能を自身で実装しなければならないのはちょっと大変ですね。Wordpressだと認証やプレビュー機能がデフォルトで備わっているので、そういった点はやっぱり便利だなと思いました。

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

Jamstack構成メディアのプレビュー機能を、サーバレスAPIを使って実装した

社内で運用するオウンドメディア開発にあたり、Jamstack構成でのプレビュー機能の開発で得た知見などをまとめておきます。構成はNext.js + Contentfulで、ホスティングサービスはAmplify Consoleを使っていますが、上記スタックに特有の機能を使ったとかではないので、他の構成でも使えるかと思います。

最初にざっくりまとめ

  • プレビューデータを取得するためのAPIをサーバレス構成で作成した
  • APIはAPI Gateway + Lambdaで作成し、リソース構築にはServerless Frameworkを利用した
  • APIの認可処理にはCognitoを利用した

Jamstackでのプレビュー機能の設計検討

ブログやオウンドメディアをJamstack構成で構築する際、基本的に各ページは事前ビルドしておき静的ページとして配信するSSGをベースに開発することになると思います。

ただ、執筆中の記事を確認する機能であるプレビュー機能については、SSGでは不可能ではないもののいまいち使い勝手が悪いものになりがちです。プレビューを確認するたびにアプリのビルドを待たなければならず、装飾などを確認したいだけなのに数分待たされるのはユーザーからすると体験は最悪です。いくら社内メンバーしか使わないといっても、不満が噴出すること間違いなしです。

SSRを使って解決する?

解決策の一つとして、プレビューページについてはSSRやクライアント側からリクエストで動的に内容を取得する方法があります。
Next.jsを使うならば、Vercelにデプロイするのが一番簡単にSSRを利用できて、手間もかからない方法です。

ただ、どんなケースでもVercelを使えるというわけではなく、弊社のケースではインフラ構成は基本的にAWSに寄せているため、今回構築するオウンドメディアについてもAmplify Consoleに乗せたいという要件がありました。

現時点ではAmplify Consoleは公式にSSRの利用をサポートしておらず(開発はしているようです: https://github.com/aws-amplify/amplify-js/issues/5435 )、うまいことやれば似たようなことはやれるのかもしれませんが、運用面を考えるとあまりハック的なことはやりたくありません。

上記の理由から、今回はSSRは採用しない方針となりました。

クライアント側から直接Contentfulなどを叩く?

クライアント側からプレビューデータを取得する方法では、ContentfulやmicroCMSのアクセスキーがブラウザに渡ってしまうという問題があります。Contentfulの場合はDelivery keyとManagement keyが分かれていて、Delivery keyが漏洩してもデータが変更されたり、破壊されたりという被害は起きないはずですが、たとえば大量のリクエストを送信してrate limitに到達させるといった悪用方法は考えられます。

Next.jsはデフォルトでバンドルファイルのチャンクをサポートしてくれるので、該当するファイルが別のファイル内から参照されない限りはチャンク後のファイルはダウンロードしないで済みます。そのため、プレビューページへのリンクをどこからも貼らないようにすれば、ブラウザにアクセスキーが渡ってしまうという問題は回避できます。

とはいえ、こういったリポジトリ特有の制約はできれば作りたくないですし、サーバーにjsファイル自体は配置されるため、URLを直接叩かれた場合は結局アクセキーの漏洩につながってしまいます(可能性自体は低いとはいえ)

上記のような懸念点が生まれるのはそもそもクライアント側だけで問題を解決しようとしているためで、素直にプレビューデータを返却するAPIを作れば万事解決するのではないかということで、今回の開発においてはプレビューデータを取得する用のエンドポイントを作成することにしました。

プレビューAPI利用におけるユーザー認証について

プレビューデータは基本的に公開してはいけないものであるため、社内の人間以外からは見えないようにする必要があります。そこでエンドポイントには認可処理を挟むこととし、それに失敗した場合は401を返却するような設計としました。

技術構成の検討

ここまでの検討で、プレビューAPIに求められる要件は以下のようなものです。

  • プレビューデータを返すエンドポイントを一つだけ実装すれば十分
  • ユーザー認証機能、およびエンドポイントでの認可処理が必要

Djangoなどを使って自前でサーバーを実装するのが素直な方法かもしれませんが、プレビュー機能のためだけに認証機能を含むサーバーを開発し、加えてインフラを整えるのも面倒です。

そこでエンドポイントについてはAPI Gateway + Lambdaで実装することとし、認証、認可機能についてはCognitoを使うことにしました。API GatewayとCognitoは簡単に連携できるのも嬉しいポイントです。

プレビューデータ取得の処理を簡単なフローチャートにすると以下のような図になります。

スクリーンショット 2020-07-24 14.54.37.png

また、これらのAWSリソース構築についてはServerless Frameworkを使うのがいいよというアドバイスをもらったので、リソース構築、管理についてはServerless Frameworkを採用することとしました。

Serverless FrameworkによるAWSリソース構築

今回の開発における必要なAWSリソースはAPI Gateway、Lambda、Cognitoの3つです。CognitoはユーザープールとIDプールがあり、「Serverless Framework Cognito」などと検索するとIDプールまで含めた構築方法の説明が多く出てきます。

ただ、今回のユースケースではユーザーの権限によって認可の範囲が異なるわけではなく、認証されているかどうかのtrue or falseの判定で十分なため、ユーザープールの作成のみで十分です。

serverless.yml

上記スタックをまとめて構築するymlファイルは以下のようなものになります。

serverless.yml
service: sample-serverless

plugins:
  - serverless-domain-manager

custom:
  stage: ${opt:stage, self:provider.stage}

  domains:
    dev: dev-preview.sample.jp
    prod: prod-preview.sample.jp

  customDomain:
    domainName: ${self:custom.domains.${self:custom.stage}}
    stage: ${self:custom.stage}
    certificateName: sample.jp
    createRoute53Record: true

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  region: ap-northeast-1
  endpointType: REGIONAL
  iamRoleStatements:
    - Effect: Allow
      Action:
        - cognito-idp:ListUsers
        - cognito-idp:AdminListGroupsForUser
      Resource: "arn:aws:cognito-idp:*"

functions:
  preview:
    handler: handler.preview
    name: ${self:service}-${self:custom.stage}
    events:
      - http:
          path: /
          method: get
          cors: true
          integration: lambda
          authorizer:
            name: ${self:custom.stage}-sample-authorizer
            type: COGNITO_USER_POOLS
            arn:
              Fn::GetAtt: [CognitoUserPool, Arn]
          request:
            template:
              application/json: '{ "slug" : "$input.params(''slug'')" }'

resources:
  - ${file(./resources/cognito-user-pool.yml)}
  - ${file(./resources/authorizer.yml)}

いくつかの箇所について解説します。

plugins:
  - serverless-domain-manager

上記の例ではAWSリソース構築とともに独自ドメインでの公開 + SSL化を行っています。それに必要なのがserverless-domain-managerで、デプロイ実行前にnpm i --save devしておけばOKです。
具体的な使い方は公式ドキュメントを参考にしてください。

functions:
  preview:
    handler: handler.preview
    name: ${self:service}-${self:custom.stage}
    events:
      - http:
          path: /
          method: get
          cors: true
          integration: lambda
          authorizer:
            name: ${self:custom.stage}-sample-authorizer
            type: COGNITO_USER_POOLS
            arn:
              Fn::GetAtt: [CognitoUserPool, Arn]
          request:
            template:
              application/json: '{ "slug" : "$input.params(''slug'')" }'

API Gateway、およびLambdaの構築に関する記述です。authorizerには同時に生成されるCognito User PoolのArnを指定しています。これだけでエンドポイントに認可機能が設定されます。めちゃくちゃ便利ですね。

また、プレビューデータを取得する際、クエリパラメータに記事のスラッグをつけてリクエストする設計にしています。Lambda関数内でそのクエリパラメータを取得できるようにするため、request以下の記述をしています。

resourceの箇所に記述しているcognito-user-pool.ymlauthorizer.ymlは以下のようになっています。

cognito-user-pool.yml
Resources:
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      AutoVerifiedAttributes:
        - email
      MfaConfiguration: "OFF"
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: false
          RequireUppercase: true
      UserPoolName: ${self:custom.stage}-sample-userpool
      UsernameAttributes:
        - email

  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: ${self:custom.stage}-sample-client
      ExplicitAuthFlows:
        - ALLOW_USER_PASSWORD_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH
      GenerateSecret: false
      UserPoolId:
        Ref: CognitoUserPool

CognitoUserPoolではパスワードの条件や各ユーザーが持つパラメータ(メールアドレスや名前など)を指定しています。今回はそれらは重要ではないので、メールアドレスだけ持っておけば十分かなと思います。

CognitoUserPoolClientでは認証手段についての設定を記述しています。重要なのはExplicitAuthFlowsで、いくつかの認証フローがあります。このへんは公式ドキュメントを見るのが早いです。

authorizer.yml
Resources:
  GatewayResponseDefault4XX:
    Type: 'AWS::ApiGateway::GatewayResponse'
    Properties:
      ResponseParameters:
        gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
        gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
      ResponseType: DEFAULT_4XX
      RestApiId:
        Ref: 'ApiGatewayRestApi

API Gatewayのオーソライザーに関する記述です。ハマったのがGatewayResponseDefault4XXの箇所で、これを記述しておかないとオーソライザーを設定した際に必ずCORSのエラーレスポンスが帰ってきてしまうため、忘れずに設定しておく必要があります。

handler.js

Lambdaにデプロイする関数を記述するファイルです。今回の例ではこんな感じになります。

handler.js
"use strict";

const axios = require("axios");

module.exports.preview = async (event, context, callback) => {
  const headers = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Credentials": true,
    "Access-Control-Allow-Headers":
      "Origin, X-Requested-With, Content-Type, Accept",
  };

  const path = `https://preview.contentful.com/spaces/${process.env.CONTENTFUL_SPACE_ID}/environments/${process.env.CONTENTFUL_ENVIRONMENT}/entries/?access_token=${process.env.CONTENTFUL_PREVIEW_KEY}&content_type=xxx%&fields.slug=${event.slug}`;

  try {
    const res = await axios.get(path);

    if (!res.data.items[0]) {
      callback(JSON.stringify({
        status: "[404]",
        message: "Not Found"
      }))
    }

    const image = res.data.items[0].fields.image
      ? res.data.includes.Asset[0].fields
      : null;

    return {
      status: 200,
      headers: headers,
      body: JSON.stringify({
        data: res.data.items[0].fields,
        image: image,
      }),
    };
  } catch (err) {
    callback(JSON.stringify({
      status: "[500]",
      message: "Internal Server Error"
    }))
  }
};

とくにややこしいことはしておらず、Contentfulのプレビューデータを取得するAPIを叩いて、帰ってきたデータを多少フォーマットして返却するだけの関数となっています。

一つ気をつける必要があるのは、エラーレスポンスの返却方法です。たとえば、以下のように記述してもクライアントには200 OKが返ってしまいます。

return {
  status: 500,
  headers: headers,
  message: "Internal Server Error"
};

これはAPI Gatewayを経由する際に、Lambdaからのレスポンスがエラーレスポンスとして認識されないため、API Gatewayが200 OKを返してしまうためです。

API Gatewayにはレスポンス内容をチェックしてレスポンスステータスを決定する仕組みがあるので、それを使ってエラーレスポンスが返るようにします。上記の例で

callback(JSON.stringify({
  status: "[500]",
  message: "Internal Server Error"
}))

status: "[500]"のようにしているのは、API Gatewayのデフォルト設定がそのようになっているためです。status: 500のようにしたい場合は、API Gateway側の設定を変更する必要があります。

デプロイの実行

serverless.ymlとhandler.jsを準備できたら、sls deploy -vと入力するだけでstageがdevの各リソースが構築されます。

package.jsonのスクリプトに

"scripts": {
  "deploy:dev": "sls deploy -v --stage dev",
  "deploy:prod": "sls deploy -v --stage prod"
}

などと記述しておけばより便利です。

クライアント側での認証機能の実装

Cognitoでの認証処理の実装については、公式がSDKを配布しているのでそれを使います(amazon-cognito-identity-js)。

各目的ごとのサンプルコードも記載されており、ほぼそのまま使うだけで認証処理ができるので便利です。今回のケースで言えば、社内の限られた人のみが使うというものなのでサインアップ機能は必要なく、ログイン機能だけあれば十分です(あらかじめユーザーは作成しておく)。

ログイン処理のサンプルコードは以下のようなものです。

Cognito.ts
import {
  CognitoUserPool,
  CognitoUser,
  AuthenticationDetails,
} from "amazon-cognito-identity-js";

const poolData = {
  UserPoolId: process.env.COGNITO_USER_POOL_ID,
  ClientId: process.env.COGNITO_CLIENT_ID,
};

const userPool = new CognitoUserPool(poolData);

export const login = async (
  email: string,
  password: string,
  newPassword: string
) => {
  if (!password) {
    return Promise.reject();
  }

  const authenticationDetails = new AuthenticationDetails({
    Username: email,
    Password: password,
  });

  const userData = {
    Username: email,
    Pool: userPool,
  };
  const cognitoUser = new CognitoUser(userData);

  cognitoUser.setAuthenticationFlowType("USER_PASSWORD_AUTH");
  return new Promise((resolve, reject) => {
    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: () => {
        resolve();
      },
      onFailure: () => {
        reject();
      },

      // 初回ログイン時のみ呼ばれるcallback
      newPasswordRequired: (userAttributes) => {
        delete userAttributes.email_verified;
        cognitoUser.completeNewPasswordChallenge(newPassword, userAttributes, {
          onSuccess: () => {
            resolve();
          },
          onFailure: () => {
            reject();
          },
        });
      },
    });
  });
};

amazon-cognito-identity-jsのようにやや巨大なライブラリを扱う場合はラッパー関数を用意する方法が便利です。テスト時のモックも簡単に実装できるようになります。

ほぼ公式ドキュメントのサンプルコードそのままなのですが、cognitoUser.authenticateUserをPromiseでラップして、関数に戻り値を設定しています。

cognitoUser.authenticateUserは最終的に引数として渡したcallback関数を実行する仕様で、戻り値がvoidなのですが、Promiseでラップするおくことで関数外部でログイン成功、失敗時の処理を分岐させることができます。イメージはこんな感じです。

try {
  await cognitoLogin(email, password, newPassword);
  setResultMessage(
        "ログインに成功しました。"
  );
} catch (err) {
  setResultMessage("ログインに失敗しました。");
}

また、cognitoUser.authenticateUserのcallbackにnewPasswordRequiredという関数を置いていますが、これはCognitoで新規作成したユーザーの有効化のためです。新規作成したユーザーは一度パスワード変更を行わないと有効化されず、初回ログイン処理の場合はこちらのnewPasswordRequiredが呼ばれるようになっています。

認証トークンの取得

上記のSDKを利用すると、ログインに成功した場合に自動的にlocalStorageに認証トークンが保存されます。それを取得するための関数も用意されています。

export const getAccessToken = () => {
  let token = "";
  const currentUser = userPool.getCurrentUser();
  if (currentUser) {
    currentUser.getSession((err: unknown, result: CognitoUserSession) => {
    if (result) {
      token = result.getIdToken().getJwtToken()
    }
  }

  return token;
};

こちらもラッパー関数を作成しました。やっていることは単純で、ログイン状態かどうかを判定し、ログイン状態であればlocalStorageからトークンを取得して返却しているだけです。

あとはAPIへのリクエスト時にヘッダに上記トークンを付加して送信すればOKです。

まとめ

プレビュー機能を作るためだけに上記のような構成を考えたりするのはなかなか大変でしたが、運用面での課題もとくになさそうで、そこそこ綺麗に設計、実装できたように感じています。

Jamstack構成はカスタマイズ性が高く、フロントエンドエンジニアにとっては魅力的ですが、多くの機能を自身で実装しなければならないのはちょっと大変ですね。Wordpressだと認証やプレビュー機能がデフォルトで備わっているので、そういった点はやっぱり便利だなと思いました。

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

Reactを初めて使ってCRUDアプリを作ってみた記録

JSフレームワークをやってみようと思い立ったので、Reactを使ってみた記録です。

マシンスペック

  • Mac mini 2018
  • macOS Catalina(10.15.x)
  • Intel Core-i7 3.2GHz 6コア
  • メモリ 32GB
  • SSD 512GB

サーバー環境

  • CentOS7.x
  • MySQL5.7.x
  • Nginx最新版
  • PHP7.1.x
  • Laravel5.6
  • Visual Studio Code最新版

フロント環境

  • React最新版
  • Bootstrap最新版
  • Visual Studio Code最新版

やること

  • ReactとLaravelでCRUDアプリを作る

補足

各環境の操作は、下記のように記載します。

[Cent]$ MacのターミナルからCentOSに接続して作業
[Mac]$ MacのターミナルでMac内の作業

前提

サーバー環境は構築済み
【コピペ】VirtualBox+Vagrant+AnsibleでLaravel開発環境を構築その弐

サーバーの準備

この記事用に作りました!
https://github.com/bobtabo/laravel5.6

[Mac]$ vagrant ssh
[Cent]$ rm -fdR laravel5
[Cent]$ git clone https://github.com/bobtabo/laravel5.6.git laravel5
[Cent]$ cd laravel5
[Cent]$ composer install
[Cent]$ chmod -R 777 storage
[Cent]$ chmod -R 777 bootstrap/cache
[Cent]$ cp -p .env.example .env
[Cent]$ php artisan key:generate
[Cent]$ vi .env
★DB設定を置換
:%s/DB_DATABASE=homestead/DB_DATABASE=hoge/g
:%s/DB_USERNAME=homestead/DB_USERNAME=fuga/g
:%s/DB_PASSWORD=secret/DB_PASSWORD=vagrant#VAGRANT1234/g
:wq
[Cent]$ bin/clear-laravel.sh
[Cent]$ php artisan migrate:refresh --seed

フロントの準備&練習

[Mac]$ brew install npm
[Mac]$ brew install node
[Mac]$ mkdir react-dev
[Mac]$ cd react-dev
[Mac]$ mkdir -p src/js
[Mac]$ npm init -y

★npm install --save-dev webpack webpack-cli webpack-dev-serverでエラーになったら行う
[Mac]$ sudo rm -rf $(xcode-select -print-path)
[Mac]$ sudo rm -rf /Library/Developer/CommandLineTools
[Mac]$ xcode-select --install

[Mac]$ npm install --save-dev webpack webpack-cli webpack-dev-server
[Mac]$ npm install -g webpack webpack-cli
[Mac]$ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader
[Mac]$ npm install --save-dev react react-dom

下記のサイトに従って、ファイルを作成する
今から始めるReact入門 〜 React の基本

  • webpack.config.js
  • src/index.html
  • src/js/client.js
[Mac]$ webpack --mode development
[Mac]$ open -a '/Applications/Google Chrome.app' ./src/index.html

※参考
https://qiita.com/TsutomuNakamura/items/72d8cf9f07a5a30be048
https://qiita.com/baby-0105/items/18f6fbc073e160bf83ac
https://qiita.com/nwtgck/items/4b9c83227bc334576552

フロントを作ってみた

上記の練習で作ったファイル(src/index.html、src/js/client.js)を色々いじってみた
https://github.com/bobtabo/react

[Mac]$ cd ..
[Mac]$ rm -fdR react-dev
[Mac]$ git clone https://github.com/bobtabo/react.git react-dev
[Mac]$ cd react-dev
[Mac]$ npm install
[Mac]$ webpack --mode development
[Mac]$ open -a '/Applications/Google Chrome.app' ./src/index.html

APIのレスポンス表示できた!
スクリーンショット 2020-07-24 15.29.33.png

※参考
https://qiita.com/dyoshikawa/items/c8b09cde728388c8feec
https://qiita.com/rei67/items/273ebef44d19912733b7

感想

LaravelをVSCodeで書いてみたけど、やっぱりPhpStormが良いかな。
プラグインあれこれ入れたけど、PhpStormの快適さを超えられなかった。

ReactでCRUDアプリ作ろう!と思ったけど、R作ったところで、お腹いっぱい。
続きは、また今度!

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

Reactプロジェクトで.envを使う

create-react-appで作成したプロジェクトであれば、プロジェクト直下に.envファイルを作成し、prefixにREACT_APP_を付けるだけで.envから変数を読み込むことができます。
参照:Reactにおける環境変数を設定について、ようやく理解したので原因と共にまとめてみる_100DaysOfCodeチャレンジ38日目(Day_38:#100DaysOfCode) - Qiita

この記事は、create-react-appを使わずに作成したReactプロジェクトで.envファイルを使う方法となります。

.envファイルの作成

プロジェクト直下に.envファイルを用意します。

API_KEY=12345678

dotnev-webpackインストール

webpackコンパイル時に.envファイルの内容をインポートしてくれるパッケージdotenv-webpackを使います。
dotenv-webpack - npm

インストール

yarn add dotenv-webpack

webpack.configの設定

const Dotenv = require('dotenv-webpack');

module.exports = {
    ...
    plugins: [
      new Dotenv(),
    ],
    ...
  };

読み込み

process.env.HOGEで読み込めます。

console.log(process.env.API_KEY);

追記:htmlファイルで.envの値を使いたい場合

html-webpack-pluginを使います。

インストール

yarn add html-webpack-plugin

webpack.configの設定

const Dotenv = require('dotenv-webpack');

module.exports = {
    ...
    plugins: [
      new Dotenv(),
      new HtmlWebpackPlugin({
        template: './src/index.html',
      }),
    ],
    ...
  };

読み込み

<%= process.env.HOGE %>で読み込めます。

<script src="http://maps.google.com/maps/api/js?key=<%= process.env.GOOGLE_API_KEY %>&language=ja"></script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactチュートリアルやってみたその2

Reactチュートリアルやってみたその2

前回は公式チュートリアルでしたが、今回はReact Tutorial: An Overview and Walkthroughというサイトのチュートリアルで各ファイルに機能を分け、中枢に当たるファイルでそれらをインポートとするようなReactアプリの作り方とnpmによるGitHub Pageに作ったReactアプリをデプロイするやり方を見ていきます。
実は、公式チュートリアルは最後に追加課題があったのですが私は知らずに飛ばしてしまったのでこちらを今回はやっています。
ここまで、やるとおそらく簡単なReactアプリを作成し、それをデプロイするまで(ただし、今回までの範囲ではサーバーを用意していないのでローカル環境のみ)のスキルが身につくはずです。

今回やること

冒頭でも書いた通りですが、前回でReactの仕組みやアプリの作り方の基本的な考え方や作り方を公式チュートリアルで学んだので、今回は少し応用として

  • コンポーネントに1つのファイルから各ファイルに分割する
  • npmでビルドする
  • GitHub Pagesにデプロイする

という以下の3点に絞って進めていきます。

コンポーネントの分割

今回は以下のようなディレクトリ構造で作っていきます

ルート
2020-07-23_12h45_15.png

srcフォルダの内部
2020-07-23_12h45_25.png

具体的にはindex.jsApp.jsをインポートし、さらにApp.jsに各コンポーネントをインポートするような形でアプリを作っていきます。
フレームワークを使ったことがある人ならわかると思いますが、前回のように1つのファイルにすべての処理を記述するようなやり方は可読性やメンテナンス性の悪さから推奨されないので、例えばDjangoであるなら1つのアプリケーションを作るにしても機能ごとにフォルダを作り、さらにその中でもコンポーネントに分割したりととかく最小単位を突き詰めていくような作り方をします。
最小単位を突き詰めれば、必然的に1つのファイルに書かれるコードは短くなり、結果どのファイルにどの処理が書いてあるかということをわかりやすくするということにつながるわけですね。

閑話休題、では今回の各ファイルを見ていきましょう。

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


// 最後に、React DOM render() メソッドを使って、作成した App クラスを HTML のルート div にレンダリングします。
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

App.js
import React, { Component } from 'react'
import Table from './Table'
import Form from './Form'

class App extends Component {
  state = {
    characters: [

    ],
  }

  removeCharacter = (index) => {
    const { characters } = this.state

    this.setState({
      characters: characters.filter((character, i ) => {
        return i !== index
      }),
    })
  }

  handleSubmit = (character) => {
    this.setState({characters: [...this.state.characters, character]})
  }

  render() {
    const { characters } = this.state
    return (
      <div className="container">
        <Table characterData={characters} removeCharacter={this.removeCharacter} />
        <Form handleSubmit={this.handleSubmit} />
      </div>
    )
  }
}

export default App

Form.js
import React, {Component} from 'react'

class Form extends Component {
    initialState = {
        name:'',
        job:'',
    }

    state = this.initialState

    handleChange = (event) => {
        const {name, value} = event.target

        this.setState({
            [name]: value,
        })
    }

    submitForm = () => {
        this.props.handleSubmit(this.state)
        this.setState(this.initialState)
    }

    render() {
        const { name, job } = this.state;

        return (
            <form>
                <label htmlFor="name">Name</label>
                <input
                    type="text"
                    name="name"
                    id="name"
                    value={name}
                    onChange={this.handleChange} />
                    <label htmlFor="job">Job</label>
                <input
                    type="text"
                    name="job"
                    id="job"
                    value={job}
                    onChange={this.handleChange} />
                <input type="button" value="Submit" onClick={this.submitForm} />
            </form>
        );
    }
}

export default Form;

Table.js
import React from 'react'

// シンプルコンポーネント

const TableHeader = () => {
    return (
        <thead>
            <tr>
                <th>Name</th>
                <th>Job</th>
                <th>Remove</th>
            </tr>
        </thead>
    )
}

const TableBody = (props) => {
    const rows = props.characterData.map((row, index) => {
        return (
            <tr key={index}>
                <td>{row.name}</td>
                <td>{row.job}</td>
                <td>
                    <button onClick={() => props.removeCharacter(index)}>Delete</button>
                </td>
            </tr>
        )
    })
    return <tbody>{rows}</tbody>
}

const Table = (props) => {
    const {characterData, removeCharacter} = props

    return (
        <table>
            <TableHeader />
            <TableBody characterData={characterData} removeCharacter={removeCharacter} />
        </table>
    )
}

// クラスコンポーネント
// class Table extends Component {
//     render() {
//         return (
//             <table>
//                 <thead>
//                     <tr>
//                         <th>Name</th>
//                         <th>Job</th>
//                     </tr>
//                 </thead>
//                 <tbody>
//                     <tr>
//                         <td>Charlie</td>
//                         <td>Janitor</td>
//                     </tr>
//                     <tr>
//                         <td>Mac</td>
//                         <td>Bouncer</td>
//                     </tr>
//                     <tr>
//                         <td>Dee</td>
//                         <td>Aspiring actress</td>
//                     </tr>
//                     <tr>
//                         <td>Dennis</td>
//                         <td>Bartender</td>
//                     </tr>
//                 </tbody>
//             </table>
//         )
//     }
// }


export default Table

Api.js

Api.jsについては最後に説明していきます。
なお、CSSについては進行上コピペで対応してくださいとのことです。
各ファイルの各コードについてはリポジトリの方に私がわかりにくかったところについて少し注釈を入れてあります。

各ファイルの役割を見ていきましょう。
今回は単純にフォームに入力されたデータをテーブルで表示して、削除もできるようにするということができるように作業を進めていきます。
index.jsは最終的なrender()を行うためのファイルです。
今回はApp.jsにアプリの機能を集約させるのでそちらのAppクラスをrender()という役割を持っています。
そのApp.jsは機能の中枢であるので各コンポーネントからデータを受け取ったり、必要に応じて各コンポーネントへデータを渡したりします。
Table.jsApp.jsからデータを受け取り、受け取ったデータをpropsに格納し、それを用いてテーブルを描画する役割を持っています。
今回、Table.jsはstateを持たないので関数コンポーネントのみで構成されています。
Form.jsは新たにデータを追加するための役割を持ちます、より具体的にはフォーム内のフィールドが変更されるたびにローカルのフォームの状態を更新し、送信時にはそのデータがすべてAppの状態に渡され、テーブルが更新されるような処理を担っています。
つまり、フォームのフィールドに応じてstate(プロパティ)が変更されますが、実際のプロパティとして確定するのは送信時のプロパティということになります。
よって、処理の流れとしては以下の通りとなります。

  1. Forms.js(データの入力)
  2. App.js(Form.jsからデータを受け取り、Table.jsパスする)
  3. Table.js(テーブルを描画する)

正確にはApp.jsにはTable.jsForms.jsそれぞれからForm・TableコンポーネントをインポートしているのでApp.jsですべて行われていることになりますが、わかりやすくいうと上記のような流れになります。
今回のミソはTable.jsForms.jsのような子コンポーネントでも複数のコンポーネントがありますが、最終的にその中でも1つのコンポーネントへデータをパスし、そのコンポーネントを親コンポーネントにインポートしているというところになりますね。
例えばTable.jsなんかはTableHeader及びTableBodyコンポーネントでマークアップしている部分がありますが、それらはすべてreturnとして返り値にされTableコンポーネントへ渡されています。
さらにTableコンポーネントでそれらは再度返り値にされることでインポート先のApp.jsへ渡され、そこで描画の処理が行われるという流れになります。
もちろん最終的な描画の処理はindex.jsで行われるわけですが、一見複雑かつ煩雑に見えるこの流れも丁寧に追っていけば理にかなっていることがわかりますね。

インポートに関してはimport App from './App'import './index.css'といったように書いていきます。
同時にインポートさせたいコンポーネントはexport default ~とコードの末尾に書いて置かなければいけません。
例えばTable.jsのTableコンポーネントをインポートしたい場合は、Table.js側でexport default Tableと定義をしておかないといけないということですね。
export default ~は1ファイルで1コンポーネントのみにしか使えません。

最後にApi.jsですがこれはReactにおいてAPIを使ったアプリケーションを作るための簡単なチュートリアルと思ってください。
今回はWikipediaのAPIを利用して、そのAPIデータをDOMにレンダリングしています。
Api.jsApp.jsの切り替えはindex.jsにおいてインポートをどちらにするかで決まります。

ビルドとデプロイ

JavaScriptやCSSは実際のページなどをDeveloper Toolなどでみると

/*! For license information pease see 2.f0b459d6.chunk.js.LICENSE.txt */
(this.webpackJsonptutorial2=this.webpackJsonptutorial2||[]).push([[2],[function(e,t,n){"use strict";e.exports=n(10)},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";function r(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function l(e,t,n){return t&&r(e.prototype,t),n&&r(e,n),e}n.d(t,"a",(function(){return l}))},function(e,t,n){"use strict";function r(e){return(r=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.

....

といったような状態で表示されることがよくあります。
これは本番環境においてはソースコード(これまで書いてきたコード)を生のままおかないということと、ReactのようなTypeScriptを使う場合はこのような形式に変換しないとブラウザ上では動作しないからです。
ソースコードをこのような形式に変換することをビルドというそうです。
React……というより、TypeScriptを使う場合はnpmの利用が不可欠になってくるので必然的にビルドはnpmが行うということになります。
といってもそんなに難しいことではなくpackage.jsonがあるディレクトリで

npm run build

を実行すればいいだけです。
ただ、今回はGitHub Pagesへのデプロイも行いたいので実行は後回しにします。

GitHub Pagesへのデプロイ

いよいよ最後の工程になります。
package.jsonを以下のように編集します

package.json

{
  "name": "tutorial2",
  "version": "0.1.0",
  "private": true,
  "homepage": "https://GitHubのユーザー名.github.io/リポジトリ名",
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "predeploy": "npm run build",
    "deploy": "gh-pages -d build"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "gh-pages": "^3.1.0"
  }
}


編集が終わったらnpmにgh-pagesをインポートします。
このライブラリを使うことでいい感じにReactがビルドされてGitHub Pagesへとデプロイされます。

npm install --save-dev gh-pages

あとは先程のコマンドを用いてビルドしてデプロイするだけです。

npm run build

npm run deploy

注意事項としては今回はサーバーを用意していないので実際にデプロイした結果を画面で確認したい場合はyarn startなどでローカルサーバーを起動しないといけないことですね。

感想

Reactチュートリアルを2つ完走した感想ですが、やっぱりJavascriptは難しい

……というのは冗談半分としてJavascriptのフレームワークという以上にできることが多いなという印象でした。
無知ゆえに、どうしてもJavaScriptには動的な描画というところにだけ意識が行ってしまうのですが、特に今回のフォームの件はDBへの保存がないだけで、やっていることはデータの追加・削除とまるでサーバーサイドでやるようなことだったのでフロントでここまでやるのかという驚きがすごいです。
だってこれおそらくAjaxでDBへ渡してしまえばサーバーサイド側でやることは受け取ったJSONデータを変換してDBへ保存したり、検索をかけて合致したものを削除するような処理を書けばいいってことになるわけで……うーん、すごい……

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

Next.jsの使い方(超基本編)

はじめに

僕の大好きな Next.js について書きます。
まだまだ新しい技術であり、最近やっと本格的に採用されるようになってきた感じがします。

とはいえ、まだまだドキュメントも少なく、初心者が始めるには公式のチュートリアルに沿って進めていくのが、最も効果的な学習方法だと思います。

ですが世の中には

ゆっくりチュートリアルなんてやってられるか!!!!
俺はとりあえず触ってみたいだけなんだ!!!!

という欲張りな方もいると思います。
今回はそんな方のために、とりあえずサクッと書き始められる までの設定をまとめたいと思います。

前提条件

npm : 6.14.5

Next.jsアプリの作成

まずはターミナルで以下のコマンドを実行して、Next.jsアプリを作成します。

$ npx create-next-app

しばらくするとプロジェクト名を聞かれるので、任意の名前を入力しましょう。
今回は sample と入力します。

? What is your project named? > sample

次にテンプレートの選択を聞かれます。
今回はまっさらな状態の物を作りたいので Default starter app を選択します。

✔ What is your project named? … sample
? Pick a template › - Use arrow-keys. Return to submit.
❯  Default starter app
   Example from the Next.js repo

success! と表示されたら完了です。

ここから、プロジェクト内の基本設定を行っていきます。

共通のレイアウトコンポーネントの作成

全てのページにメタデータなどの共通情報を書き込むのは非効率ですよね。
共通のレイアウトコンポーネントを作成しましょう。

プロジェクト直下に component ディレクトリを作成して、 layout.js を作成しましょう。

$ mkdir component
$ cd component
$ touch layout.js

layout.js のなかでレイアウトコンポーネントを作成します。

layout.js
import Head from 'next/head'
import Link from 'next/link'

export default function Layout({ children }) {
  return (
    <div>
      <Head>
        <link rel="icon" href="/favicon.ico" />
        // 共通のメタデータなどはここに記載
      </Head>
      <main>
        {children} // ここにコンテンツが埋め込まれるイメージ
      </main>
    </div>
  )
}

プロジェクト直下の pages/index.js を作成したレイアウトに埋め込みます。

index.js
import Head from 'next/head'
import Layout from '../components/Layout' // 作成したレイアウトコンポーネントを読み込む

export default function Home() {
  return (
    <Layout>  // レイアウトタグでコンテンツを囲む
      <div>
      </div>
    </Layout>
  )
}

これで共通のレイアウトコンポーネントの作成が完了しました。

CSSファイルの作成

最初の状態だとCSSがファイル内に書かれていたと思います。
でも、やはりCSSは別ファイルで管理したいですよね。

グローバルCSSの設定

ここでは、すべてのページで読み込まれるCSSを設定します。
全てのページで読み込む必要のないCSSは後述します。

まずはプロジェクト直下の pages ディレクトリに _app.js というファイルを作成します。
(先ほどのコマンドでcomponentディレクトリにいる人はpagesディレクトリに移動してください)

$ touch _app.js

次に styles ディレクトリを作成して global.css 作成します。
(先ほどのコマンドでpagesディレクトリにいる人はプロジェクト直下に移動してください)

$ mkdir styles
$ touch global.css

先ほど作成した _app.js ファイルでコンポーネントを作成していきます。
このコンポーネントは、全てのページに共通するトップレベルのコンポーネントになります。
つまり、ここでの設定は全てのコンポーネントに共通して設定されます。

_app.js
import '../styles/global.css'

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />
}

これでグローバルCSSファイルを読み込むことができるようになりました。

コンポーネント別CSSファイルの作成と読み込み

先ほど作成したCSSファイルは全てのコンポーネントで読み込まれてしまうため、そこに全てのCSSを書いてしまうのは良い手法とは言えません。

コンポーネント別にCSSを適応してみましょう。

先ほどの styles ディレクトリ内に style.module.css というファイルを作成します。
(先ほどのコマンドでpagesディレクトリにいる人はプロジェクト直下に移動してください)

$ touch style.module.css

このように、CSSを使う際は CSSモジュール を利用します。
そのため、ファイル名の末尾は module.css でなければなりません。

あとは使いたいコンポーネント内で、モジュールをインポートするだけです。

layout.js
import styles from './layout.module.css' // CSSモジュールを読み込む
import Head from 'next/head'
import Link from 'next/link'

export default function Layout({ children }) {
  return (
    <div>
      <Head>
        <link rel="icon" href="/favicon.ico" />
        // 共通のメタデータなどはここに記載
      </Head>
      <main className={styles.sample}> // クラス名はこのように読み込む
        {children}
      </main>
    </div>
  )
}

以上で基本的な設定は終わりです。

まとめ

Next.jsは初心者でも触りやすくReactの経験者であればかなり恩恵を受けられる素晴らしいものだと思います。

環境構築で苦戦していてはもったいないので、是非気軽に触ってみてださい。

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

React初心者覚書07 - フォーム ー

Reactの記事を参考に初心者がわからないけど覚書を書いています。

FORM

  • Formは内部になんらかの状態をもっているので、他のDOM要素とは少し異なる
html_form.html
<form>
  <label>
    Name:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>

  • フォームの送信に応答してユーザがフォームに入力したデータにアクセスするような JavaScript 関数があった方が便利
  • "制御された (controlled) コンポーネント” と呼ばれるテクニックを使う

制御されたコンポーネント= React で値を制御している入力フォーム要素のこと

  • HTML では 、、そして のようなフォーム要素は通常、自身で状態を保持しているが、 Reactでは、変更されうる状態は通常はstateプロパティに保持され、setState()関数でのみ更新される。

this.handleChange = this.handleChange.bind(this);がわからない

NameForm.handleChange = NameForm.handleChange.bind(NameForm);ってこと?

でも見てたら何をしているかはなんとなく理解。
フォームイベントが起こると、 NameFormクラス自身の中で、イベントの内容に合わせて変更されるってこと?

nameform.js
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

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

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