7839

雑草魂エンジニアブログ

【React】React + GSAP(TextPlugin)で1文字ずつ表示されるアニメーションを実装する(Next.js)

先日に引き続き、今回も GSAP で実装したアニメーションをご紹介。 今回は、タイプライター風アニメーションというのか、1 文字ずつ文字が表示されるアニメーションを実装した。

f:id:serip39:20201222214133g:plain

TextPlugin

greensock.com

TextPlugin は、GSAP のプラグインの 1 つである。このプラグインの特徴は以下の通りである。

  • DOM要素にテキストコンテンツを 1 文字ずつ埋め込む(区切り文字をスペースなどに設定することで、1 単語ごとに埋め込むこともできる)
  • アニメーションが終了すると、DOM要素にはテキストが埋め込まれて、完全に置き換えられる
  • 表示速度を設定できる

使い方

import { gsap } from 'gsap'
import { TextPlugin } from 'gsap/TextPlugin'

useEffect(() => {
  if (process.browser) {
    gsap.registerPlugin(TextPlugin)
    setAnimation()
  }
}, [])

const setAnimation = () => {
  gsap.to("#テキストを挿入するDOM要素", {
    duration: 2, //アニメーション時間(秒)
    text: {
      value: "This is the new text", //表示するテキスト
      delimiter: "",  //区切り文字
    },
    ease: "ease",  // アニメーションのタイミング・進行割合を指定する
  })
}

詳細は、公式ドキュメント を参照して欲しい。

React での実装例

React で実装するにあたり、TextAnimationコンポーネントを作成した。

テキストを埋め込む領域を確保しておかないと、改行されるたびに表示領域が大きくなるので、事前に Height を確保することにした。処理の流れは以下の通りである。

  1. DOMがレンダリングされる
  2. DOM内のテキストを読み込む
  3. DOMの高さを取得する
  4. DOM内のテキストを削除し、高さを設定する
  5. scrollTrigger をトリガーとして、テキストアニメーションを行う
import { ReactElement, useCallback } from 'react'
import { gsap } from 'gsap'
import { TextPlugin } from 'gsap/TextPlugin'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

type Props = {
  children: React.ReactNode
  section: string
}

const TextAnimation = (props: Props): ReactElement => {
  const textRef = useCallback((node) => {
    if (node !== null) {
      const text = node.innerHTML  //テキストを読み込む
      const height = node.clientHeight  //高さを取得する
      node.innerHTML = ''  //テキストを削除する
      node.style.height = height + 'px'  //高さを設定する
      setAnimation(text)
    }
  }, [])

  const setAnimation = (text) => {
    const numText = text.length
    const selector = '#' + props.section

    gsap.registerPlugin(TextPlugin)
    gsap.registerPlugin(ScrollTrigger)
    gsap.to(`${selector} .animation-text`, {
      duration: numText * 0.03,
      text: {
        value: text,
      },
      ease: 'none',
      scrollTrigger: {
        trigger: selector,
        start: 'top 40%',
        end: 'bottom 40%',
      },
    })
  }

  return (
    <p ref={textRef} className="animation-text">
      {props.children}
    </p>
  )
}

export default TextAnimation

今回、useCallback を使って、コールバック形式の ref を使っている。最初は、useRef を使って実装したが、その場合に TextAnimation のコンポーネントを使い回すことができなかった。(どのコンポーネントでも、同じテキストが表示されてしまった。)

useRef の特徴は以下の通りである。

  • useRef は毎回のレンダーで同じ ref オブジェクトを返す。
  • useRef は中身が変更になってもそのことを通知しない。.current プロパティを書き換えても再レンダーは発生しない。

詳細は、公式ドキュメント useRef 参照。

実際に、TextAnimation を使う場合は、以下のようにする。

import { ReactElement } from 'react'
import TextAnimation from '~/components/TextAnimation'

const Component = (): ReactElement => {
  return (
    <section id="souseki">
      <TextAnimation section="souseki">
        吾輩は猫である。
        <br />
        名前はまだ無い。どこで生れたかとんと見当がつかぬ。
        <br />
        何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
        <br />
        吾輩はここで始めて人間というものを見た。
        <br />
        しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。
        <br />
        夏目漱石
      </TextAnimation>
    </section>
  )
}

export default Component

f:id:serip39:20201222214133g:plain

まとめ

GSAP の TextPlugin の使い方を簡単に紹介した。簡単にタイプライター風のアニメーションが実現できて、便利であった。

また、今回の実装を通して、React の useRef やコールバック形式の ref についても学ぶことができた。

それでは、ステキな開発ライフを。

関連する記事

serip39.hatenablog.com

【React】React + GSAP(ScrollTrigger)でスクロールアニメーションを実装する(Next.js)

スクロールアニメーションを実装するにあたり、GSAP の ScrollTrigger を使うことで簡単に実装できたので、今回はその実装方法を紹介する。

GSAP とは

GSAP(GreenSock Animation Platform)は、高速・軽量のHTML5 アニメーションライブラリである。主なモジュールとして、アニメーションを実装する TweenMax とそのアニメーションのタイムラインを制御する TimelineMax がある。そして、2020年5月、スクロールアニメーションのためのプラグインライブラリ「ScrollTrigger」がリリースされた。

greensock.com

ScrollTrigger の特徴

様々な機能が盛り沢山であるが、簡単に便利だと思った機能を紹介する。

  • 特定の要素に、それぞれ個別にアニメーションを設定できる
  • トリガー位置を柔軟に設定できる
  • 特定のスクロール位置で要素を固定する、ピン留めも設定可能
  • onEnter, onLeaveなどのcallbackが設定できる
  • トリガー位置のマーカー表示が簡単にできる
  • パフォーマンス最適化

詳細は、公式ドキュメント を参照して欲しい。

React での実装例

インストール

GSAPのモジュールをインストールする。(詳細は、Docs 参照。)

$ yarn add gsap

使い方

gsap.registerPlugin() メソッドで、ScrollTrigger を登録する。(実装では、Next.js を使用したため、client側でのみ実行したいため、process.browsertrue の場合のみ登録している。)

import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

useEffect(() => {
  if (process.browser) {
    gsap.registerPlugin(ScrollTrigger)
    setAnimation()
  }
}, [])

const setAnimation = () => {
  gsap.to('#アニメーションさせたい要素', {
    // アニメーション内容
    scrollTrigger: {
      trigger: '#トリガー要素',
      start: 'top 40%',  //開始時のトリガー条件
      end: 'bottom 40%',  //終了時のトリガー条件
      onEnter: () => {}, //スクロールイン時
      onEnterBack: () => {}, //スクロールバック時
      markers: true // マーカー表示
    }
  })
}

gsapのメソッドはたくさんあるので、ここでは主に使うものだけを紹介する。

  • gsap.to():(現在の状態) → Bになる状態を設定する
  • gsap.from():A → (現在の状態)になる状態を設定する
  • gsap.fromTo():A → Bになる状態を設定する
  • gsap.timeline():タイムライン制御をする際に用いる
const tl = gsap.timeline({repeat: 2, repeatDelay: 1})
tl.to("#id", {x: 100, duration: 1})
tl.to("#id", {y: 50, duration: 1})
tl.to("#id", {opacity: 0, duration: 1})

そして、gsapのメソッドの引数である、第二要素の「アニメーション内容」の部分に、scrollTrigger を定義する。

ちょっとわかりにくい、アニメーションの開始時/終了時のトリガー条件は、以下の通りである。

start: "${#トリガー要素の基準点} ${#ブラウザの画面の位置}"

start: 'top 40%'の場合、「トリガー要素のトップが、ブラウザのトップから40%のところにきたら、アニメーション開始」となる。

参考例

以下のコードは、スクロールに応じて、文字が下から上にフワッと浮き上がってくるようなアニメーションである。

import { ReactElement, useEffect } from 'react'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

const Component = (): ReactElement => {
  useEffect(() => {
    if (process.browser) {
      gsap.registerPlugin(ScrollTrigger)
      setAnimation()
    }
  }, [])

  const setAnimation = () => {
    gsap.fromTo(
      '#wrapper-a p',
      { opacity: 0, y: 10 }, //fromの設定
      {  //toの設定
        opacity: 1,
        y: 0,
        duration: 2,
        scrollTrigger: {
          trigger: '#wrapper-a',
          start: 'top center', //要素のトップが、画面の中央まできたら開始
          end: 'bottom center', //要素のボトムが、画面の中央まできたら終了
          onEnter: () => {
            console.log('scroll In')
          },
          onEnterBack: () => {
            console.log('scroll Back')
          },
        },
      }
    )
  }
  return (
    <div className="wrapper" id="wrapper-a">
      <p>TEST ANIMATION</p>
    </div>
  )
}

export default Component

まとめ

GSAP の ScrollTrigger の使い方を簡単に紹介した。細かい設定までできるので、使い勝手がよく、とても便利であると思えた。

それでは、ステキな開発ライフを。

【JS / React】ブラウザバックを制御する

今回、HP 内で意図的にブラウザバックを制御して、UX を向上させられるようにしたので、実装方法を紹介する。

ブラウザバックの制御に関して

ブラウザバックを制御するためには、セッション履歴を操作する必要がある。制御の流れは以下である。

  1. ページがレンダリングされたら、ダミーのセッション履歴を追加しておく
  2. ユーザーがブラウザの「戻る」ボタンを押す(1. でダミーの履歴を挿入していたので、実際にはブラウザバックが動作しない)
  3. popstateイベントが発火する
  4. 条件に応じて、以下を実行する
     a. ブラウザバックをする
     b. 任意の操作をする(再度、ブラウザバックをさせないように、ダミーのセッション履歴をまた追加しておく。)

ブラウザのセッション履歴の操作は、History API で実施する。

ダミーのセッション履歴は、History.pushState() を用いる。

history.pushState(null, null, null)

実装例

今回示す例では、同ページに、Step1 と Step2 のコンポーネントがあり、stateのstepに応じて、コンポーネントを切り替える。

ただし、step2 の場合、ブラウザバックを禁止して、ブラウザバックせずに、step1に戻るようにする。(特にスマホの操作において、ブラウザバックでstep1に戻るようにしておくと、UXが向上する。)

import { ReactElement, useEffect, useRef, useState } from 'react'
import Step1 from '~/components/Step1'
import Step2 from '~/components/Step2'

const Component = (): ReactElement => {
  const [step, setStep] = useState(1)
  const stepRef = useRef(null)

  useEffect(() => {
     history.pushState(null, null, null)
    stepRef.current = step
    window.addEventListener('popstate', overridePopstate, false)
    return () => window.removeEventListener('popstate', overridePopstate, false)
  }, [])

  useEffect(() => {
    stepRef.current = step
  }, [step])

  const overridePopstate = () => {
    if (stepRef.current === 2) {
      history.pushState(null, null, null)
      setStep(1)
    } else {
      history.back()
    }
  }

  return (
    <section>
      <div className="wrapper">
        {step === 1 ? (
          <Step1 goToNext={setStep(2)} />
        ) : (
          <Step2 goToBack={setStep(1)} />
        )}
      </div>
    </section>
  )
}

export default Component

特筆すべきは、リスナ内関数では state の変更を参照することができず、値渡しになっている。そのため、useRef を用いて、ref.current で参照するようにした。

まとめ

History API があることは知っていたが、今回初めて使って実装を行った。ブラウザバックに関して、意図したように制御ができてよかった。

それでは、ステキな開発ライフを。

【GitHub Actions】Next.js + Firebase Hosting をビルド&デプロイする

GitHub Actions は、最高に便利である。
今回は、Next.js で作った HP を Firebase Hosting に自動デプロイする GitHub Actions を紹介する。

GitHub Actions の構成

今回、Actions のトリガーは以下の2つを想定している。

  • main ブランチに push された場合
  • Contentful で、記事が更新された場合(repository_dispatch:GitHubのWebhookイベントを使用する)

そして、処理の流れは以下の通りである。

  1. リポジトリをチェックアウトする
  2. node環境のセットアップ
  3. yarn install
  4. Google Cloud SDKをセットアップする
  5. envファイルをダウンロードする
  6. yarn build & yarn export(SSG)
  7. Firebase Hosting にデプロイする
  8. Slackにデプロイ完了を通知する

workflowの新規作成

GitHub 上でも、Actions のタブから workflow の template を選択して、新規作成することができる。(とてもわかりやすくて、便利。)ただ、私はローカルのエディタで作成して、GitHub に push を行った。

1. root ディレクトリに、.github/workflowsフォルダを作成する。(←フォルダ名が「workflow」の場合、Actionsとして認識されないので、注意。私は一度フォルダ名を間違えて、末尾のsを抜かしてしまい、やらかしたw)

$ sudo mkdir -p .github/workflows

2. 任意の名前で設定ファイル(.yml)を作成する。今回は、Firebaseへのデプロイ用だったため、firebase.ymlとした。

$ sudo touch firebase.yml
name: Deploy to Firebase Hosting

on:
  push:
    branches:
      - main
  repository_dispatch:

jobs:
  deploy:
    name: Build & Deploy
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - name: Check out code
        uses: actions/checkout@v2.0.0
      - name: Set up Node
        uses: actions/setup-node@v2.1.2
        with:
          node-version: ${{ matrix.node-version }}
      - name: Get yarn cache
        id: yarn-cache
        run: echo "::set-output name=dir::$(yarn cache dir)"
      - uses: actions/cache@v2
        with:
          path: ${{ steps.yarn-cache.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-
      - name: yarn install
        if: steps.yarn-cache.outputs.cache-hit != 'true'
        run: yarn install --frozen-lockfile
      - name: Set up gcloud CLI
        uses: google-github-actions/setup-gcloud@master
        with:
          version: 'latest'
          service_account_email: ${{ secrets.GCP_SA_EMAIL }}
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          export_default_credentials: true
      - name: Download local environment variables
        run: |
          gsutil cp gs://<---storage path---> .env
      - name: yarn build & export
        run: yarn export
      - name: Deploy to Firebase
        uses: w9jds/firebase-action@master
        with:
          args: deploy --only hosting
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
      - name: Notification to Slack
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
        uses: pullreminders/slack-action@master
        with:
          args: ‘{\”channel\”:\”<--channelId-->\",\"text\”:\”Firebaseへのデプロイが完了しました\”}’

特筆しておくべきところは特にないが、actions/cache を使って、キャッシュを再利用することでビルドを効率的にしている。

依存関係をキャッシュしてワークフローのスピードを上げる 参照

また、設定ファイルの中に出てくる${{ secrets.XXXX }}GitHubのSettings > Secrets の Repository secrets に登録しておく必要がある。

  • GCP_SA_EMAIL
  • GCP_SA_KEY
  • FIREBASE_TOKEN
  • SLACK_BOT_TOKEN

以降で、設定する項目の取得方法を紹介する。

GCPの設定

setup-gcloud GitHub Action 参照

1. GitHubActions用のサービスアカウントを発行する

基本的には、上記のリンク先の操作方法を参照して欲しい。 ただ、ロール設定に関しては、今回 Cloud Storageの読み取りができればいいので、その権限のみを付与する。

f:id:serip39:20201219203624p:plain

2. サービスアカウントのキーを発行する

以下のようなjsonファイルがダウンロードされる。

{
  "type": "service_account",
  "project_id": "project-id",
  "private_key_id": "key-id",
  "private_key": "-----BEGIN PRIVATE KEY-----\nprivate-key\n-----END PRIVATE KEY-----\n",
  "client_email": "service-account-email",
  "client_id": "client-id",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-account-email"
}

3. GitHub の Secrets に登録する

GCP_SA_EMAIL は、上記のclient_emailGitHub の Secrets にコピーするだけでいい。

GCP_SA_KEY は、ダウンロードしたサービスアカウントキーを base64 エンコードした文字列をいれる必要があるので、以下のコマンドを実行する。

$ cat service-account-key.json | base64 | pbcopy

上記コマンドを実行することで、クリップボードエンコードされた文字列が保存されるので、あとは GitHub の Secrets に貼り付けるだけでいい。

Firebaseの設定

GitHub Action for Firebase を参照

GCP_SA_KEY を取得しているので、サービスアカウントの権限を追加することで、FIREBASE_TOKEN は不要であるが、今回Cloud Storageの権限しか付与しなかったので、FIREBASE_TOKEN を取得するようにした。

ローカルで、以下のコマンドを実行する。

$ firebase login:ci

✔  Success! Use this token to login on a CI server:
1//****
Example: firebase deploy --token "$FIREBASE_TOKEN"

ブラウザが開き、ログインすることで、トークンが発行される。この FIREBASE_TOKENGitHub の Secrets に登録する。

Slack Appの設定

Post Slack messages 参照

1. ワークスペースで利用するボットの作成

2. GitHub の Secrets に登録する

Slack App の OAuth & Permissions にある OAuth Tokens for Your Team の Bot User OAuth Access Token の値をコピーして、GitHub の Secrets に登録する。

3. チャンネルIDを取得する

アプリの場合、Slack Bot のメッセージを投稿したいチャンネルを選択して、右クリック > リンクをコピー をする。

https://XXXX.slack.com/archives/C0154PW1XUM

末尾の「C0154PW1XUM」がチャンネルIDとなる。

これで全ての設定が完了である。

処理実行

実際に、mainブランチに push して動作確認を行う。

実行結果に関しては、GitHub の Actions で確認することができる。エラーが発生した場合は、各実行項目でのエラー内容なども確認できるので、内容に応じて修正を行う。

まとめ

GitHub Actions の設定方法を紹介した。
正直、これが最適なのかはまだわかっていない。今後も、より最適な方法を模索していきたい。

それでは、ステキな開発ライフを。

【SSH】踏み台サーバーを経由した多段SSH接続のやり方(.ssh/configの利用)

最近、サーバー運用などを勉強している。

VPC 環境を構築する時に、サブネットを多段に分割してセキュリティレベルを分け、踏み台サーバー経由でしかSSH接続できないようにする構成が多いようだ。

踏み台サーバーを経由した多段 SSH 接続で、AWS(EC2)で運営されているサーバーにアクセスしたので、備忘録として残しておく。

(通常の)SSH 接続

まずは、通常の SSH 接続方法を確認する。

$ ssh -i [秘密鍵] [ユーザー名]@[アドレス]

# EC2の場合の例
$ ssh -i ~/.ssh/config.d/aws.pem ec2-user@xxx.xxx.xxx.xxx 

この場合、毎回秘密鍵のパスやログインユーザー名、アドレスを思い出して、入力するのは、面倒である。そこで、.ssh/config に設定を記載しておくことで、ホスト名のみでSSH接続ができるようにしておく。(秘密鍵は、.ssh/config.d に入れるようにした。)

$ sudo mkdir ~/.ssh/config.d
$ sudo vim ~/.ssh/config
Host appX-server
  HostName xxx.xxx.xxx.xxx
  User ec2-user
  IdentityFile ~/.ssh/config.d/aws.pem
  StrictHostKeyChecking no
  IdentitiesOnly yes

このように設定しておくことで、以下のようにして SSH 接続することができる。

$ ssh appX-server

非常に便利である。

多段 SSH 接続

いよいよ本題である。

構成

まずは、簡単であるが、構成図を示す。public の踏み台サーバーを経由して、アプリケーションサーバーにアクセスする。

f:id:serip39:20201219000241p:plain

接続方法

基本的には、.ssh/config に設定を書いて接続を行うが、上記に沿って、コマンドで接続する方法も記載しておく。

ProxyCommand を使う。

$ ssh -i [(app)秘密鍵] -o ProxyCommand='ssh -i [(step)秘密鍵] -W %h:%p [(step)ユーザー名]@[(step)アドレス]' [(app)ユーザー名]@[(app)アドレス]
  • -W:フォワードの設定を行う
  • %h:ホスト
  • %p:ポート番号

上記が置換されることで、多段 SSH 接続ができる。

また、.ssh/config は以下の書くことができる。

Host step-server
  HostName xxx.xxx.xxx.xxx
  User ec2-user
  IdentityFile ~/.ssh/config.d/aws.pem
  StrictHostKeyChecking no
  IdentitiesOnly yes

Host appX-server
  HostName xxx.xxx.xxx.xxx
  User ec2-user
  IdentityFile ~/.ssh/config.d/application.pem
  ProxyCommand ssh -W %h:%p step-server
  StrictHostKeyChecking no
  IdentitiesOnly yes
$ ssh appX-server

.ssh/config なくしては、踏み台を介した SSH 接続はできないですね。

まとめ

今回は、サーバーアクセスの要である SSH 接続について整理してみた。

それでは、ステキな開発ライフを。