20201215のJavaScriptに関する記事は30件です。

create-react-app Advanced Configuration まとめ 2020年末版

進化し続けるReact。
React大好きなんですがちょっと離れてると機能が増えてたり、変更があったりと追いつくのがなかなか大変です。
今年はAdvanced Configurationで廃止された設定、新しく追加された設定に絞って書いてみます。
一部、勉強のためにcreate-react-appの修正箇所も紹介します。

2019年末版はこちら

Advanced Configurationの公式ドキュメント

GitHubのドキュメント

create-react-app

設定方法

shellで設定する場合には

package.json
  "scripts": {
    "start": "CI=true react-scripts start",

のようにするか、.envファイルで

.env.development
CI=true

のように環境ごとに設定します。

廃止されたAdvanced Configuration

NODE_PATH

react-scripts@4.0.0で廃止。
今後はjsconfig.jsonで設定してねってこと。
もともと、baseUrlを設定してない場合には、NODE_PATHを使うようになってたのでやっとなくなったのかという設定。

https://github.com/facebook/create-react-app/releases/tag/v4.0.0

EXTEND_ESLINT

これも同じくreact-scripts@4.0.0で廃止。
この設定がなくても拡張できるようになったとのこと。

修正PR

https://github.com/facebook/create-react-app/releases/tag/v4.0.0

追加されたAdvanced Configuration

WDS_SOCKET_HOST

WDSwebpack-dev-serverから。
Hot Module Replacement用にカスタムのwebsocketホスト名で開発サーバーを実行。
webpack-dev-serverのデフォルトはwindow.location.hostnameで、SockJSのホスト名を指定します。
この変数を使用して、一度に複数のプロジェクトでローカル開発を行える。

https://webpack.js.org/configuration/dev-server/#devserversockhost

WDS_SOCKET_PATH

webpack-dev-serverのデフォルトは、SockJSパス名の/sockjs-node
この変数を使用して、一度に複数のプロジェクトでローカル開発を行える。

https://webpack.js.org/configuration/dev-server/#devserversockpath

WDS_SOCKET_PORT

webpack-dev-server のデフォルトは、SockJS ポートの window.location.port です。
この変数を使用して、一度に複数のプロジェクトでローカル開発を開始を行える。

https://webpack.js.org/configuration/dev-server/#devserversockport

FAST_REFRESH

Fast Refreshという実験段階の機能。
アプリ全体をリロードするわけではなく、修正したコンポーネントのみを更新することにより高速開発が可能になる。
問題があったらfalseに設定してオフにしましょう。

DISABLE_NEW_JSX_TRANSFORM

React 17 で導入され、React 16.14.015.7.00.14.10 にバックポートされたNEW JSX TRANSFORMを無効にします。
古いReactプロジェクトでReactをバージョンアップできなく問題になる場合に使用する。

NEW JSX TRANSFORMについて

まとめ

どうでしたか?
設定の廃止、追加をみるだけでも今年1年のReactの変化が見れて面白いですね!

今年追加された設定は、自分の開発ではあまり使う機会はないかも。
廃止された設定は使っていたのでそっと削除しておこうと思います。

来年もReactはどのように変化していくのか楽しみ!

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

【Vue.js】Local Storageでデータを永続化させる

Local Storageとは

JavaScriptを使ってWebブラウザにデータを保存できる仕組みです

5~10MB程度のデータを永続化できます

保存された内容はChrome DevToolsで確認できます
スクリーンショット 2020-12-15 16.33.06.png

今回はVue.jsで見ていきます

保存する

保存するにはlocalStorage.setItem()を使用します

第1引数にキー
第2引数にバリュー
を渡します

<template>
  <div class="container">
    <button @click="set()">保存</button>
  </div>
</template>

<script>
export default {
  methods: {
    set() {
      localStorage.setItem('name', '田中')
    }
  }
}
</script>

簡単に「保存ボタン」が押されたらデータを保存するようにします

demo


第2引数に複数の値を渡す場合はJSON.stringify()を使用しJSON文字列へ変換する必要があります

<template>
  <div class="container">
    <button @click="set()">保存</button>
  </div>
</template>

<script>
export default {
  methods: {
    set() {
      localStorage.setItem('obj', JSON.stringify({
        id: 1,
        name: 'tanaka',
        age: 20
      }))
    }
  }
}
</script>

demo


取得する

データを取り出すにはlocalStorage.getItem()を使用します
現状JSON形式でデータが保存されているのでJSON.parse()でJavaScriptのオブジェクトに変換する必要があります

mountedにデータを取得する記述を用意しレンダリングします

<template>
  <div class="container">
    <button @click="set()">保存</button>
    <p>{{this.info.id}}</p>
    <p>{{this.info.name}}</p>
    <p>{{this.info.age}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      info: {}
    }
  },
  mounted() {
    this.info = JSON.parse(localStorage.getItem('obj'))
  },
  methods: {
    set() {
      localStorage.setItem('obj', JSON.stringify({
        id: 1,
        name: 'tanaka',
        age: 20
      }))
    }
  }
}
</script>

スクリーンショット 2020-12-15 23.33.07.png

永続化されているのでリロードしても消えません

削除する

最後です

localStorage.removeItem();で引数に渡したキーに紐づく値を削除できます。

また、localStorage.clear();で全ての値を削除できます

localStorage.removeItem('obj');

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

【写真とコード付き】Vue.jsでフェードイン・フェードアウトする簡単は方法


はじめに

この記事はVue.jsの基本は抑えられている程で、話を進めていきます。もしVue.jsを初めて触ると言う方は、こちらの記事を参照していただければと思います。

⬇️【写真とコード付き】Vue.jsの構築から基本的な書き方まで1から解説【超初心者向け】
https://qiita.com/yuki4839/items/62f40564e3f4c8dbfc51

さて今回は、Vue.jsでフェードイン・フェードアウトの仕方について、ご紹介させていただこうと思います。

早速いきましょう。



実行環境

使用ツール、デバイスはこちら

  • Google chrome
  • Mac OS Catalina
  • Visual Studio Code


また今回使用するディレクトリ階層はこちら。

ディレクトリ階層
─ root(任意のディレクトリ)
│
├─ index.html
│
├─ css
│   └ style.css
│
└─ js
    └ main.js


各ファイルの初期値はこちら。

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <link rel="stylesheet" href="./css/style.css">
</head>

<body>

  <div id="web">
    <p>
      {{ context }}
    </p>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script src="./js/main.js"></script>
</body>

</html>
style.css
/* 出力結果を見やすくするためのスタイルです */
body {
  background-color: #add8e6;
}

#web {
  background-color: #fff;
  margin: 20px;
  padding: 20px;
  width: 300px;
}
main.js
const web = new Vue({
  el: '#web',
  data: {
    context: `Hello Vue.js!`
  }
})


現時点での出力結果はこちら。

スクリーンショット 2020-12-12 21.13.46.png



transition

単刀直入に言うと、フェードイン・フェードアウトを行うには、マウントしたHTMLタグ内で transitionタグ を利用することによって作成することができます。

まずは実際のコードをご覧ください。

index.html
<!-- headタグ省略 -->
<body>

  <div id="web">
    <button v-on:click="show=!show">
      Click
    </button>
    <transition>
      <p v-show="show">
        This sentence is a Transition.
      </p>
    </transition>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script src="./js/main.js"></script>

</body>
style.css
body {
  background-color: #add8e6;
}

#web {
  background-color: #fff;
  margin: 20px;
  padding: 20px;
  width: 300px;
}

.v-enter-active,
.v-leave-active {
  transition: 3s;
}

.v-enter,
.v-leave-to {
  opacity: 0;
}
main.js
var web = new Vue({
  el: '#web',
  data: {
    show: false
  }
})

手順

①まず v-on:click="show=!show" (falseとtrueの切り替え) でクリックイベントを作成。
②次に v-show="show" でクリックイベントの対象要素を指定。
③クリックイベントの対象要素を transitionタグ で囲む。

動作手順

①Vue.js の data が false に設定されているので、pタグ には display: none;当たっている。
②buttonタグ のボタンをクリックすると、pタグ の display: none;外れる。
③display: none; が外れると同時に、transitionタグ の配下の要素(上記なら pタグ)に、 class="v-enter-active v-enter"当てられる。
④イベントが終了したら、class="v-enter-active v-enter"外れる。
⑤再度 buttonタグ のボタンをクリックすると、transitionタグ の配下の要素(上記なら pタグ)に、 class="v-leave-action v-leave-to"当てられる。
⑥イベントが終了したら、class="v-enter-active v-enter"外れdisplay: none;当てられる。

動作確認

まずデフォルトはこちら。

スクリーンショット 2020-12-15 23.18.23.png

ボタンをクリックすると、ゆっくりフェードイン。

スクリーンショット 2020-12-15 23.19.45.png

しばらくすると描画完了です。

スクリーンショット 2020-12-15 23.18.39.png

再びボタンを押すと、ゆっくりフェードアウト。

スクリーンショット 2020-12-15 23.19.45.png

しばらくすると描画完了です。

スクリーンショット 2020-12-15 23.18.23.png




まとめ

今回は Vue.js でのフェードイン・フェードアウトの方法を解説いたしました。ぜひ実際にコードを書いて見てください!

最後まで読んでいただき、ありがとうございました!





筆者:yuki|IT業界のリアルな転職事情など発信|元トップ営業マン(訪販)→未経験からエンジニア転職へ
Qiita:https://qiita.com/yuki4839
Twitter:https://twitter.com/yukifullstack

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

Node.jsを使ってYoutubeの一部データをスプレッドシートに書き出してみる

はじめに

こちらは、CODE BASE OKINAWA プログラミングスクール Advent Calendar 2020の12/15の記事になります。

プログラミング歴5ヶ月目にして初めてこのような記事を書くことになります。せっかくなので、自分の作りたいものをこの機会に作ってシェアする感じで書いていきます。今回は、Node.jsでYoutubeAPIデータをスプレッドシートに書き出すについて書いていきます。

私自身、フロントエンドの学習自体はもうすぐ2ヶ月近くになり、普段はVueを触っていて、最近はAPIについても色々勉強しています。まだ技術的なことは書けないのですが、何かを作ったりするのは好きなので、この題材にさせていただきました。
対象レベルは、プログラミング初学者やフロントエンド学習者、何か作りたい!といった方、あたりに参考なれば嬉しいです。

node.jsにした理由

  1. 普段からパッケージ管理で、npmを利用してるのでライブラリの種類や情報の掴み方を知っているから
  2. jsでwebアプリケーションぽく動作させたいから

vueでやってもよかったのですが、普段から使っているのでたまにはnode.jsで書いてみたかったてのもあります。

node.jsについて詳しく書かれています
Node.jsとはなにか?なぜみんな使っているのか?

実行環境

$ node -v
v14.15.0

$ npm -v
6.14.8

作業ディレクトリの作成

$mkdir node-sheet
$cd node-sheet
$npm init -y
Wrote to node-sheet/package.json:

{
  "name": "node-sheet",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

$ ls
package.json

YoutubeAPI有効化

GoogleAPIConsole
https://console.developers.google.com/

スプレッドシートに書き込むデータを取得するためにAPIを有効化します。
こちらを参考にしました。

GoogleSpreadSheetAPI有効化

GoogleAPIConsole
https://console.developers.google.com/

以下のサイトが参考になるかと思います。
https://qiita.com/howdy39/items/ca719537bba676dce1cf

指定のスプレッドシートにアクセスするために必要な認証情報を発行します。
ここでは、サービスアカウントを利用してスプレッドシートにアクセスし、
編集権限に必要な情報をAPIに持たせることができます。そのためのjsonファイルが発行され、ダウンロードされるはずなので、作業ディレクトリに移動させます。

//client_secret.jsonに変更して移動させる
$ls
package.json
$ls        
client_secret.json  package.json
  • 認証情報を作成する
  • サービスアカウントのメールを後ほど使います。

スクリーンショット 2020-12-15 21.46.59.png

Spreadsheetの設定

  • 以下のように1行目のA~E列までカラム名を書いておきます
    スクリーンショット 2020-12-15 23.19.42.png

  • 共有に先程のサービスアカウントのメールを貼り付けます。共有には編集権限を持たせておいて下さい。
    スクリーンショット 2020-12-15 22.01.01.png

モジュールのインストールとファイルの作成

$touch spreadsheet.js

//スプレッドシートへの認証やアクセスに必要
$ npm install google-spreadsheet

//youtubeAPIを叩くのに必要
$ nom install axios

spreadsheet.js

const { GoogleSpreadsheet } = require('google-spreadsheet');
const { promisify } = require('util')
const axios = require('axios') 

const CREDIT = require('./client_secret.json') //認証情報をCREDITに持たせる

// スプレッドシートキー
const SPREADSHEET_KEY = 'スプレッドーシートのURL=>https://docs.google.com/spreadsheets/d/<この部分を書く>/edit#gid=0'

const accessSpredsheet = async function (spreadsheetKey, keyword) {

  let Datas = []; // youtubeのデータを取得後に入れる変数
  // youtubeのクエリデータを指定()
 // keywordは関数呼び出し時に引数として指定
  const params = {
    q: keyword, //キーワード
    part: 'snippet', //どの部分のデータが欲しいのか
    type: 'video',
    order: 'viewCount', //並び順
    maxResults: '22', //取得するデータの数
    key: '<APIキーはここに書く>'
  }
  axios
    .get('https://www.googleapis.com/youtube/v3/search', {
      params: params
    })
    .then(function (response) {
      console.log(response)
      Datas = response.data.items //Datasに取得したデータを入れる
      console.log(Datas)
    })
    .catch(function (error) {
      console.log(error)
    })


  //spreadsheetの指定
  const doc = new GoogleSpreadsheet(spreadsheetKey) 

  // サービスアカウントによる認証
  await doc.useServiceAccountAuth({
    client_email: CREDIT.client_email,
    private_key: CREDIT.private_key,
  });

  // spreadsheetの情報を読み込み
  const info = await doc.loadInfo();; 
  const sheet = doc.sheetsByIndex[0];
  console.log(sheet.title) //スプレッドシートのタイトル
  console.log(sheet.rowCount) //行の数

  //Datasにあるyoutubeデータをスプレッドシートに挿入する
  Datas.forEach(Data => {
    sheet.addRow({
      title: Data.snippet.title, //タイトル
      description: Data.snippet.description,  //説明
      date: Data.snippet.publishTime,  //投稿時間
      channelTitle: Data.snippet.channelTitle,  //チャンネルタイトル
      thumbnail: Data.snippet.thumbnails.medium.url  //サムネイル
    })
  })
}

//上記の関数を呼び出し
accessSpredsheet(SPREADSHEET_KEY, "Hikakin");

書き出し実行

最後に、spreadsheet.jsを実行させて、取得したyoutubeデータをスプレッドシートに書き出していきます。
今回は、「Hikakin」というキーワードで実行しました。

$ node spreadsheet.js

youtubeAPIからは以下のようなデータが複数取得されます。

{
    kind: 'youtube#searchResult',
    etag: 'zlk1vpMDWLcNHr51wCs5jvtFBRM',
    id: { kind: 'youtube#video', videoId: 'qBQ5w7RwVnI' },
    snippet: {
      publishedAt: '2019-12-15T03:00:15Z',
      channelId: 'UCg4nOl7_gtStrLwF0_xoV0A',
      title: 'ヒカキン &amp; セイキン - 夢',
      description: 'HIKAKIN #SEIKIN #夢 【Music】 監修:HIKAKIN 作詞作曲:SEIKIN 編曲:TeddyLoid 【Music Video】 Director:ZUMI Producer:Sakura Wakatsuki (avex) Director of ...',
      thumbnails: [Object],
      channelTitle: 'SeikinTV',
      liveBroadcastContent: 'none',
      publishTime: '2019-12-15T03:00:15Z'
    }
  },

最終的にこんな感じになります。
スクリーンショット 2020-12-15 20.00.05.png

まとめ

少し大雑把だったかもしれません。。。
外部APIを使って情報を取得したり、アクセストークンを使ってspreadsheetの書き出しをしたり、意外と学べる要素が多かったかなと感じてます。APIを叩くということも楽しいのですが、今回は、クライアント側で認証情報をリクエストして、GoogleSpreadsheetAPIで認証情報を発行して、DBにアクセスできる、といった流れがいかにもアプリケーションらしくてフロントエンド初学者に知っておいていい仕組みかなとも感じてます。
個人的には、Oauthについて勉強しようと思えた機会でした。
ちょうどフロントエンドとサーバーサイド間の学習を発展させたかったので良かっです。

参考記事

Youtube api 認証キー設定
http://piyohiko.webcrow.jp/kids_tube/help/index.html

GoogleSpreadsheetAPI有効化
https://qiita.com/howdy39/items/ca719537bba676dce1cf

GoogleAPIConsole
https://console.developers.google.com/

【Vue.js】YouTube Data APIをaxiosで取得し表示するサンプル(Firebase・Vue CLI v4.0.4)

Node.jsとはなにか?なぜみんな使っているのか?

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

GCP運用を便利にするHyperのpluginを作った話

こんにちは、オールアバウト SRE所属の @ishii1648 です。

この記事は、All About Group(株式会社オールアバウト) Advent Calendar 2020 16日目の記事です。

概要

オールアバウトではクラウドインフラとしてGCPを利用しており、私の所属しているSREチームは各サービスの環境を横断的に管理しています。

普段の仕事の流れはDEV環境用のプロジェクトで検証を進め、検証が完了したらSTG環境、本番環境にリリースするというものですので、多くの時間gcloudコマンドの接続先はDEV環境のプロジェクトに向いています。しかしリリース後に接続先を戻し忘れると、本番環境に接続したままになるので気をつけないと事故に繋がる恐れがあります。

戻し忘れや、コマンド実行時に注意すれば済む話しですが、自分の注意力に期待するよりも環境改善するほうがエンジニアとして健全だろうと思い Terminal の Plugin を作ることにしました。

前提

  • Hyper を使ったことがある(インストールはしたぜ!くらいでも大丈夫です)
  • Javascript の基本を理解している

Plugin の機能概要

  • 接続先のプロジェクトを表示
  • 接続先のKubernetesコンテキストを表示
  • DEV環境以外のプロジェクト接続時はColorスキーマを変更

Pluginを作るまでの経緯

Hyperでは多くのPluginが公開されているので自作する必要も無いかなと思いましたが、上記の仕様を完全に満たすPluginが無かったので結局自作することになりました。

ただし類似のPluginは既に存在したので、フォークする形で利用させて頂きました。
ここで紹介させて頂く内容の基本実装は全て下記リポジトリ開発者の方に依るものです。

https://github.com/marcjoha/hyper-gcp-status-line

Plugin の作り方

Hyperは Electron & React で動作しています。Reactの実装ではHOC(Higher-order Components)が採用されており、PluginではHOCで返されてきたコンポーネントをカスタマイズする事でHyperの動作に干渉することができます。

プロジェクトの作り方

以下、公式で非常に丁寧に説明されていますので、詳しくは以下をご覧ください。

https://github.com/ishii1648/hyper/blob/canary/PLUGINS.md

ここでは最低限必要な内容を解説していきます。

1. Hyperを開発モードで起動する

まずは公式のHyperをフォークします。
(必須ではありませんが、後でコードを追加するのでフォークしておいたほうが管理上都合が良いです)

https://github.com/vercel/hyper

上記でフォークしたリポジトリをローカル環境にcloneし、canaryブランチにcheckoutした後にリポジトリ直下で以下コマンドを実行すると開発モードで起動できます。

$ yarn
$ yarn run app

開発モードで動作できることを確認したら一旦、停止しておきます。

2. 設定ファイルを配置する

ユーザディレクトリ直下に配置されている設定ファイル(.hyper.js)をリポジトリ直下にコピーし、以下のように開発用の設定を追加してください。

module.exports = {
  config: {
    ...
  },
  plugins: [],
  localPlugins: ['プラグイン名'], <-- 追加する箇所
  ...
}

3. Pluginを配置する

Hyperは開発モードでの起動時に以下ディレクトリに配置されている内容をlocalPluginとして読み込もうとします。

.hyper_plugins/local/

なのでここに開発するPluginを配置します。以下は私の実際の開発環境です。

スクリーンショット 2020-12-15 6.12.02.png

直接 .hyper_plugins/local/ 配下にプロジェクトを作成してしまっても良いのですが、それだと何かと不便なので私の場合は別ディレクトリにプロジェクト作成して以下コマンドでシンボリックリンクを貼っています。

ln -s ~/src/github.com/ishii1648/hyper-gcp-status-line ~/src/github.com/ishii1648/hyper/.hyper_plugins/local/

4. 実装

ここまで来たら後はひたすらコーディングです。今回は私の作った Plugin のコードをもとに Hyper の CSS を動的に変更する方法を解説します。

以下リポジトリと動作イメージになります。

https://github.com/ishii1648/hyper-gcp-k8s-info-line/

hyper-gcp-k8s-info-line.gif

それではコードの解説を始めていきます。

まずは表示内容を記述する部分です。表示用のAPIは幾つか用意されていますが、以下では decorateHyper を利用しています。Terminalの下部に表示したかったので、footerにDOMを追加しています。

この辺りの用意されているAPIや画面のDOM構成はHyperの公式ページで解説されています。

exports.decorateHyper = (Hyper, { React, notify }) => {
    return class extends React.PureComponent {
        constructor(props) {
            super(props);
            this.state = {};
        }

        render() {
            const { customChildren } = this.props;
            const existingChildren = customChildren ? customChildren instanceof Array ? customChildren : [customChildren] : [];

            return (
                React.createElement(Hyper, Object.assign({}, this.props, {
                    customInnerChildren: existingChildren.concat(React.createElement('footer', { className: 'hyper-gcp-status-line' },
                        React.createElement('div', { className: 'item gcp-project', title: 'GCP project' }, this.state.gcpProject),
                        React.createElement('div', { className: 'item kubernetes-context', title: 'Kubernetes context and namespace' }, this.state.kubernetesContext),
                    ))
                }))
            );
        }

        componentDidMount() {
            // Check configuration, and kick off timer to watch for updates
            setConfiguration();
            this.repaintInterval = setInterval(() => {
                this.setState(state);
            }, 100);
        }

        componentWillUnmount() {
            clearInterval(this.repaintInterval);
        }
    };
}

ここで Redux の処理に介入しています。GCPプロジェクトのDEV環境以外への変更を検知したら、本番兼STG環境用のカラースキーマを dispatch します。

exports.middleware = (store) => (next) => (action) => {
    switch (action.type) {
        case 'SESSION_ADD_DATA':
            if (action.data.indexOf('\n') > 1) {
                setConfiguration();

                if (state.isChangeEnv) {
                    store.dispatch({
                        type: 'CONFIG_RELOAD',
                        config: { prdColorScheme: productionColorScheme }
                    });
                    state.isChangeEnv = false
                }
            }
            break;

        case 'SESSION_SET_ACTIVE':
            setConfiguration();
            break;
    }

    next(action);
}

プロジェクトの変更検知は以下のGCPプロジェクト取得処理時に併せて実施しています。

function setGcpProject() {
    exec("cat " + configuration.gcpConfigurePath + " | grep project", (error, stdout, stderr) => {
        if (error) {
            state.gcpProject = 'n/a';
            return
        }

        oldGcpProject = state.gcpProject
        project = stdout.split("=")[1].trim()
        state.gcpProject = project

        if (oldGcpProject != state.gcpProject) {
            state.isChangeEnv = true
        }
    })
}

dispatchされたカラースキーマは以下の reduceUI で取得して state に追加します。

exports.reduceUI = (state_, { type, config }) => {
    switch (type) {
        case 'CONFIG_LOAD':
            if (config.hasOwnProperty('hyperGcpKubernetesInfoLine')) {
                Object.assign(configuration, config.hyperGcpKubernetesInfoLine)
            }
            let initialColorScheme = {}
            Object.keys(productionColorScheme).forEach((key) => {
                initialColorScheme[key] = state_[key]
            })
            return state_.set('initialColorScheme', initialColorScheme)
        case 'CONFIG_RELOAD': {
            if (config.hasOwnProperty('hyperGcpKubernetesInfoLine')) {
                Object.assign(configuration, config.hyperGcpKubernetesInfoLine)
            }
            if (!config.hasOwnProperty('prdColorScheme')) {
                return state_
            }
            if (state.gcpProject.indexOf(configuration.devGCPProjects) > -1) {
                return state_.set('prdColorScheme', {empty: true})
            }

            return state_.set('prdColorScheme', config.prdColorScheme)
        }
    }

    return state_
}

最後に以下で props にマージしてやります。これで Hyperの CSS が書き換わります。

exports.mapTermsState = (state, map) => {
    if (!state.ui.prdColorScheme) {
        return map;
    }
    if (Object.keys(state.ui.prdColorScheme).indexOf('empty') > -1) {
        return Object.assign({}, map, {profiles: state.ui.initialColorScheme});
    }
    return Object.assign({}, map, {profiles: state.ui.prdColorScheme});
}

exports.getTermGroupProps = (uid, parentProps, props) => {
    const {profiles} = parentProps
    if (!profiles) {
        return props
    }
    const profileProps = Object.assign({}, profiles);

    return Object.assign({}, props, profileProps);
};

Pluginの公開方法

HyperのPluginの実体はnpmのモジュールです。なのでnpmモジュールとして公開することでインストールできるようになります。
npmの公開方法は以下記事で詳しく解説されています。

https://qiita.com/TsutomuNakamura/items/f943e0490d509f128ae2

まとめ

Hyper は Javascript で作られているため、多くのエンジニアにとってカスタマイズがしやすいのが素晴らしい点です。また、今回ご紹介したように非常に簡素な実装で Plugin を作ることができるため、Javascriptにそれほど精通していない方でも見様見真似で充分実装できます。

何より自分で実装したツールが生産性を改善してくれるのはとても気持ちの良いものです。是非これを機に Hyper の Plugin を開発してみてください。

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

【BigQuery】Javascript UDFで外字変換してみた

概要

興味本位で調べた、BigQueryのJavascript UDFを使用して、BigQueryのテーブルデータの外字を内字に変換する方法をメモします。

外字を使用したデータの取得

クエリ

1文字目を「希」、2文字目を外字にして取得する。

SELECT '\u5E0C\uE757' AS char

結果

2文字目が外字のため、画面上だと文字化けしている。
スクリーンショット 2020-12-15 19.08.39.png

外字を内字に変換してデータ取得

クエリ

Javascript UDFで、外字と内字のマッピング表を定義し、外字を内字に変換する。

CREATE TEMP FUNCTION CONVERT_EXTERNAL_CHAR(input STRING)
RETURNS STRING
LANGUAGE js AS
"""
const convert_list = [
{ gaiji: "\uE757",naiji: "\uFA93"} // 望
];

convert_list.forEach(function(item,index,array) {
  input = input.replace(item.gaiji, item.naiji);
});

return input;
""";
SELECT
  CONVERT_EXTERNAL_CHAR(char) AS char
FROM
  ( SELECT '\u5E0C\uE757' AS char );

結果

「希望」と出力された!
スクリーンショット 2020-12-15 19.09.09.png

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

Qiita殿堂入りアプリを作りました(二番煎じ)

はじめに

Qiita殿堂入りアプリを作りました。
暇な時にご活用ください。

Qiita殿堂入りのホームページ

https://matt-note.github.io/qden/
スクリーンショット 2020-12-17 1.38.27.png

デザインは阿部寛のホームページを参考に作成しています。
デザインをAbehiroshize(アベヒロシャイズ)することにより、実行速度と体感速度の向上を実現しています。

PWAに対応しているので、スマホのホーム画面に追加することができます。
Screenshot_20201218-020658.jpg

もうあるやん?

Qiitaの殿堂入りについては、すでにQiitaの殿堂というサイトがあります。

このサイトがあると初めて知った時にアクセスしたのですが、数日間アクセスできなかったので、「サービス終了してしまったのか。じゃあ自分で作ろう。」と考えたのが、Qiita殿堂入りアプリを作成するキッカケでした。(後になってから上記サイトが復旧してアクセスできることを知りました…。)

技術構成

とにかく運用コストを抑えたかったので 「GitHub Pagesを使用 + GitHub ActionsのcronでQiitaの月ごとのランキングを毎日自動取得してJSONファイルに書き出す」構成にしました。JSONファイルはcurljqで作成しています。

アプリでは作成済みJSONファイルをJavaScriptで取得してHTMLに表示しています。フロントエンドもライブラリのバージョン対応をしたくなかったので、Vanilla JavaScriptで作成しています。

仕様

1. 殿堂入りの定義

Qiita殿堂入りのホームページでは、殿堂入りの定義を「ストック数が262以上のもの」としました。これはQiita APIにクエリを投げる時に、ストック数で絞り込みができるための都合です。ストック数なので、LGTM数ではありません(LGTM数では絞り込みができないため)。そのため、262より少ないのLGTM数であってもストック数が262以上であれば殿堂入りとしてカウントします。

262という数字については、イチローが2004年に達成したシーズン262安打を基準としています。イチローだったら米国野球殿堂入りするだろうということで、この数字をQiita殿堂入りの基準にしています。

2. 表示LGTM数

作成済みのJSONファイルを表示しているので、リアルタイムのLGTM数を表示しているわけではありません。あくまで参考としてご活用ください。

3. 月の記事上限数

月の記事上限数は100件までです。これは1回のリクエストで100件まで取得しているためです。(Qiita APIの仕様。このため、ストック数が262以上でも100件より先は表示されません)

4. Qiita APIの仕様

表示についてはデータ取得元のQiita APIの仕様に基づいています。とにかく、Qiita APIが返した値を表示しているので、「11月に投稿した記事が10月に表示されている」ということがありえます。これは11月1日の0:15に投稿した場合など、月の境界部分についてはこういうことがありえます。(細かいことは不明)

5. 作成日時

殿堂入りの年月は作成日時を基に表示しています。限定公開してから公開した場合は、限定公開した最初の時が作成日時となります。

6. 更新日時

殿堂入りした記事の中で、作成日時と更新日時の年月が違う場合に、更新日時を表示する仕様としています。同じ年月で日にちが違う時に更新した場合、更新日時は表示されない仕様です。

7. 取得期間

Wikipediaによると、Qiitaが公開されたのは2011年9月16日とのことなので、2011年9月から条件を満たす記事を取得しています。

こういう事さ!

ここまでをザックリまとめると下記の図となります。
GitHub Actionsのcroncurlを実行し、Qiita APIにアクセスします。ストック数が262以上のものを取得し、jqで整形してJSONという気味の悪い拡張子で書き出します。その後はcommitpushしてリポジトリを更新しています。これを毎日深夜に自動で実行しています。
アプリからは作成済みJSONファイルを取得して描画しています。

B5CA7MyCIAI_O2K.jpg

開発にまつわるエトセトラ?

ここからはアプリ作成での学びを記載します。

1. quicklink

Qiita殿堂入りのホームページで唯一使用しているJSライブラリがquicklinkです。
これは画面表示領域のリンクをprefetchしてくれるライブラリで、ユーザがリンクをクリックした場合に、prefetch済みデータを読み込むので処理速度が向上します。これはAbehiroshize(アベヒロシャイズ)するための必須ライブラリでした。使い方はREADME.mdに書かれている通りです。

// 'load'を指定するのがポイント。リンクのprefetchは画像などページのすべてが読み込まれた後で良いため、
// DOMContentLoaded(HTML文書の解析完了時)でない方が良い。
// 細かいカスタマイズはREADME.mdを参照。
window.addEventListener('load', () =>{
  quicklink.listen();
});

2. Qiita APIで「以上」「以下」を指定できない

Qiita APIで指定するクエリでは>=の指定ができません。そのため、262以上のストックがある記事を絞り込みたい場合は261より多いを指定します。なおかつ、11月1日からデータを取得したい場合、開始日付は10月31日のように月末を指定します。(query=created:>2020-11-01だと、11月1日の記事が含まれない。)

# 例
curl -G \
  --data-urlencode "query=created:>2020-10-31 created:<2020-12-01 stocks:>261" \
  --data-urlencode "page=1" \
  --data-urlencode "per_page=10" \
https://qiita.com/api/v2/items | \
jq '. | map({ title: .title?, url: .url?, likes_count: .likes_count?, created_at: .created_at?, updated_at: .updated_at?, id: .user.id?}) | sort_by(.likes_count) | reverse'

なお、上記コマンドの実行結果に2020年11月01日 06時13分に作成されたPythonのオブジェクト指向プログラミングを完全理解の記事が含まれていません。この記事は2020年10月の記事と判定されています。このあたりはQiita APIの仕様です。(細かいことは不明

3. curlの--data-urlencodeオプション

上記の例のとおり、curlの--data-urlencodeを指定すると、URLエンコードした文字を指定する必要がないので、コードの可読性が向上します。直書きでURLエンコードする場合は、下記のように記述することになります。(下記リンク押下でアクセス可能。10件のみ取得)

https://qiita.com/api/v2/items?query=created%3A%3E2020-10-31%20created%3A%3C2020-12-01%20stocks%3A%3E261&page=1&per_page=10

4. Macのdateコマンド

MacのdateコマンドはBSD Unixベースのため、GNU Linuxのdateコマンドと差異があります。GitHub ActionsではUbuntuを使いたかったので、Macで作成したスクリプトをUbuntuで実行できず、困りました。(ローカルで確認できず、GitHub Actionsを動かして動作確認するハメに…。パソコンの買い替え時ですかね…。)

5. GitHub ActionsのcronはUTC

JST(Japan Standard Time: 日本標準時)でcronを実行したい場合は、UTCから9時間差し引いた時間を指定します。環境変数でタイムゾーンを設定すれば、そのままJSTで指定できるのではと思いましたが、できませんでした。

ちなみにUTC(協定世界時)は英語で"Coordinated Universal Time"なので、頭文字はCUT。フランスが"Temps Universel Coordonné"の頭文字であるTUCを提案してきたので、なんやかんやあってみんなで仲良くUTCを使うようになった。
・参考: UTC(協定世界時)は何の頭文字か

# 例 JSTの0:00に実行する場合、UTCの15:00を指定する
on:
  schedule:
    - cron: '0 15 * * *'

6. classList

JavaScriptでHTML要素の表示・非表示を切り替えるためにclassを操作したい時はclassListが大活躍してくれました。toggle()で指定したclassの切り替えができて、contains()で指定したclassを持つか判定することができます。

See the Pen yLaMzVm by Shinichi Matsumoto (@matt-note) on CodePen.

7. QiitaにCodePenを貼り付けできる

QiitaにCodePenを貼り付けできますが、ちょっとしたコツが必要です。
上のCodePenは下記コードを貼り付けたものです。

<p class="codepen" data-height="344" data-theme-id="light" data-default-tab="js,result" data-user="matt-note" data-slug-hash="yLaMzVm" style="height: 344px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="yLaMzVm">
  <span>See the Pen <a href="https://codepen.io/matt-note/pen/yLaMzVm">
  yLaMzVm</a> by Shinichi Matsumoto (<a href="https://codepen.io/matt-note">@matt-note</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<!-- デフォルトの下記scriptタグではQiitaで表示できない -->
<!-- <script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> -->
<!-- 下記コードに修正することでQiitaでCodePenを表示できる (細かいことは不明) -->
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

さいごに

Qiita殿堂入りのホームページ(Q殿)のソースコードは下記の設計図共有サイトで管理しています。スターを付けてもらえると励みになります?

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

GatsbyでSEO対策をする

メタデータを追加するための準備

yarn add gatsby-plugin-react-helmet react-helmet

gatsby-config.jsにgatsby-plugin-react-helmetの設定を追加する

gatsby-plugin-react-helmet

このプラグインを使って、タイトルやメタ属性などの属性をコンポーネントに追加すると、それらがGatsbyが構築する静的なHTMLページに追加されます。これはサイト閲覧者にとってだけでなく、SEOにとっても重要です。

react-helmet

headタグの管理をするコンポーネント

メタデータの設定を記述するファイルを準備

import React from "react";
import {Helmet} from "react-helmet";

class Application extends React.Component {
  render () {
    return (
        <div className="application">
            <Helmet>
               <html lang="言語の種類" />
         <meta charSet="utf-8" />
               <title>My Title</title>
         <meta name="description" content="説明" />
               <link rel="canonical" href="http://mysite.com/example" />
            </Helmet>
            ...
        </div>
    );
  }
};

サイト全体で使用するメタデータの値を用意する

サイト全体で繰り返し使用したいメタデータは、siteMetadataとして用意しておくとGraphQLのクエリで取得できるようになる

gatsby-config.js

module.exports = {
/* Your site config here */
siteMetadata: {
title: `Example`,
description: `Example description`,
lang: `ja`,
siteUrl: `https://*******.netlify.app`
},
plugins: [
...

ページごとにメタデータの値を変える

example.js

export default ({data}) => (
  <Layout>
    <SEO
      pagetitle="example"
      pagedesc="example description"
    />
   <div>
   ...

参考文献:
Webサイト高速化のための静的サイトジェネレーター活用入門
react-helmet: https://github.com/nfl/react-helmet
gatsby-plugin-react-hemet: https://www.gatsbyjs.com/plugins/gatsby-plugin-react-helmet/

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

【自分用】VueがIE11で動かなかった

WebpackとBabelを通しているがIE11で何も表示されなくなった

SCRIPT1002: Syntax error

Eval関数のところでエラーが出ていたので、webpackに以下の設定を追加してみたが、直らなかった

webpack.config.js
module.exports = {
    //...
    devtool: 'none'
}

原因はvueの読み込みところだった

script.js
import Vue from 'vue/dist/vue.esm.browser.min.js'

以下に変更

script.js
import Vue from 'vue'

resolve.alias の設定がされていないので、追加

webpack.config.js
module.exports = {
    //...
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
        }
    }
}

resolve.alias の指定は以下の記事を参考にした
https://blog.websandbag.com/entry/2020/08/07/190655

他のPJでは動いていたので、根本的な原因は別のところにあると思うが、とりあえず解決したのでここまで

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

文字を1文字ずつ表示する

作成したもの

transformを使って、文字列を1文字ずつ表示させると同時に表示した文字を上下させる

用いた言語

js、jqeury

完成したコード

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./jump_text.css">
  <script src="../../jquery/jquery-3.5.1.min.js"></script>
</head>

<body>
  <div class="one_dis">
    <p class="btn" id="btn">testボタン</p>
    <p class="text" id="text">1文字ずつテキストを表示します</p>
    <div class="jump_char" id="jump_char">
      <p class="text add_text" id="add_text"></p>
    </div>
  </div>

  <script>
    $(function() {
      $('#btn').click(function() {

        //表示する文字列のタグを設定しておく
        let get_text = $('#text').text();
        //countで文字毎にクラス設定するための変数
        let count = 0;
        //文字を動かす量の変数
        let move_char = 20;
        //文字を追加する間隔の変数
        let addTime_char = 500;
        //文字が動く時間の変数 ※addTIme_charより大きくした場合正常な動きをしない可能性あり
        let moveTime_char = 1000;
        //文字を上に移動させる時間、下に移動させるタイミングの設定
        let moveUpTime_char = 100;
        let moveDownTime_char = 200;

        //メインの関数
        //のちにclearsetITmeOutを使うため変数に代入しておく
        var text_display = function() {

          //文字列を1文字ずつ追加するための用意
          let add_char_tag = $('<span class="char" id="char"></span>');
          add_char_tag.addClass('char' + count);
          $('#add_text').append(add_char_tag);

          // 文字の単体のcssを設定
          //inline-blockを指定することでtransformを有効化
          $('.char' + count).css({
            display: 'inline-block',
            opacity: 0,
            transform: 'translateY(' + move_char + 'px)',
            transition: moveTime_char + "ms"
          });
          //charAt関数を用いて1文字ずつ文字をspanタグに追加
          $('.char' + count).text(get_text.charAt(count));

          //setTimeout関数を用いて、時間差をつけ文字を上下させる
          setTimeout(moveUp($('.char' + (count - 1))), moveUpTime_char);
          setTimeout(moveDown($('.char' + (count - 2))), moveDownTime_char)


          count = count + 1;
          var setTimeout_id = setTimeout(text_display, addTime_char);
          //文字列の長さまでcountが進んだらストップ
          if (count > get_text.length) {
            clearTimeout(setTimeout_id);
          }
        }
        //発火させるアクションがないので関数を呼び出す
        text_display();

        function moveUp(charNum) {
          $(charNum).css({
            opacity: 1,
            transform: 'translateY(0px)',
            transition: moveTime_char + 'ms'
          });
        }

        function moveDown(charNum) {
          $(charNum).css({
            transform: 'translateY(' + move_char + 'px)',
            transition: moveTime_char + 'ms'
          });
        }
      });
    });
  </script>
</body>

</html>

動きが見れるものも置いておきます。

See the Pen ZEpeWJG by ぺこ太? (@pecotaro1089) on CodePen.

個人的にハマった個所

setTImeout関数を複数用いた際のtransformの挙動

上に動いた文字を再び下に戻すために、setTimeout関数の中にsetTImeout関数を使い、その中でcssメソッドを使うと何故かtransformだけ上手く動かなくなった。
対策:setTimeOut関数の引数を関数として定義し、入り子ではなく別々に使う。(理由がわからない)

transformが効かなくなった

transfromはインラインブロックで動くらしく、spanタグでは動かないらしい。
対策:spanタグにdisplay: inline-blockを追加することで解決。

transformにdelayが効かない

cssだけか分からないが、delayが効かないメソッドがあるらしい。
対策:setTimeout関数を使う。

最後に

2か月前からコーディングを始めた初心者で今回の記事が初めてなので、コード自体とても拙く、見づらい部分が多々あると思います。
この記事は、自分が見直す際に使えるような目的に置いているため、丁寧すぎる部分、わかりづらい部分もあると思いますが、目をつぶってくれたら幸いです。

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

JavaScript基礎文法入門を焼肉で(2)

この記事について

JavaScript基礎文法入門の、学習内容のアウトプット・自身の振り返り用のメモ2です。
メモ1:JavaScript基礎文法入門を焼肉で(1)

参考資料
Progate JavaScript(ES6)学習コース
ドットインストール 詳解JavaScript 基礎文法編

【クラス】

Webサービスなどでは、オブジェクトをいくつも扱っている
毎回ゼロから作成していては大変なので、似たようなデータを効率よく作成する必要がある

そのための方法として、最初に「設計図」を用意する方法がある
例えばユーザー(利用者)のデータ(オブジェクト)をいくつも作成する場合、最初に「ユーザーを生成するための設計図」を用紙し、その設計図を基にユーザーのデータを生成していく、といったことができる。

この設計図のことをJavaScriptでは「クラス」と呼ぶ
クラス名は基本的に「大文字」から始める

class Meat {
}

インスタンスの生成

クラスからオブジェクトを生成するには「new クラス名( )」とする
クラスから生成したオブジェクトは「インスタンス」と呼ぶ

class Meat {
}
const meat = new Meat(); //Meatインスタンス
console.log(meat);

コンストラクタの処理

コンストラクタの中には処理を記述できる
この処理は、インスタンスが生成された直後に実行され、
インスタンスごとに毎回実行される

class Meat {
    constructor () {
      console.log("美味しい!");
    }
}
const meat1 = new Meat();
const meat2 = new Meat();

「this.プロパティ = 値」とすることで
生成されたインスタンスにプロパティと値を追加できる
インスタンスとはオブジェクトなので、コンストラクタの中で追加した値は
「インスタンス. プロパティ」とすることでクラスの外で使用できる

class Meat {
  constructor() {
    this.name = "hatsu"; //プロパティと値
  }
}
const meat = new Meat();
console.log(meat.name); //コンストラクタで設定した値が使える

コンストラクタでは引数を受け取ることが可能
constructorの後の( )に引数を記述することで
その引数をコンストラクタの処理内で使用できる

class Meat {
  constructor(name, price) { //引数を受け取る
    this.name = name;
    this.price = price;
  }
}
const meat = new Meat("hatsu", 900);
//文字列"hatsu"という値が引数として渡され、コンストラクタ内ではnameとして使える

【メソッド】

メソッドは、そのクラスから生成したインスタンスに対して呼び出す
「インスタンス. メソッド名( )」とすることでそのメソッドを呼び出し処理を実行できる

class Meat {
  constructor(name, price) {
    |
  }
  greet() {           //メソッド名
    console.log("実質タダ!");  //処理
  }
}
const meat = new Meat("hatsu", 900);
meat.greet(); //メソッドの呼び出し

メソッド内でインスタンスの値を使用するには「this.プロパティ名」とする

class Meat {
  |
  info() {
    consolo.log(`名前は${this.name}です`);
   //インスタンスのnameプロパティの値になる
  }
}
const meat = new Meat("hatsu", 900);
meat.info();

メソッド内で他のメソッドをを呼び出すことも可能
メソッド内で「this.メソッド名( )」とすることで同じクラスの他メソッドを使用できる

class Meat {
  greet() {
    console.log("実質タダ!");
  }
  info() {
    this.greet(); //同じクラスのメソッドを実行
      |
  }
}

【継承】

既にあるクラスを基に、新しくクラスを作成する方法
継承を用いてクラスを作成するには「extends」を用いる

class Beef extends Meat { //Meatクラスを継承
}
//基となるクラスを親クラス、新しく作成するクラスを子クラスと呼ぶ

子クラスは親クラスのすべての機能を引き継いでいる
そのため、子クラス内には何もメソッドが定義されていなくても
親クラスに定義されているメドッドは使用できる

class Meat {
  constructor(name,price) {
    this.name = name;
    this.price = price;
  }

  greet() {
    console.log("いらっしゃいませ!");
  }

  info() {
    this.greet();
    console.log(`${this.name}ですね`);
    console.log(`${this.price}円です`);
  }
}
class Beef extends Meat {
}
const beef = new Beef("和牛タン", 3000);
beef.info(); //Meatクラスに定義されているメソッドを使用できる

継承したクラスでの独自メソッド

継承したクラスにもメソッドを追加できる
関数と同じように戻り値を用いることもできる

class Beef extends Meat {
  many() {
    return this.price *2; //メソッドでも戻り値は使える
  }
}

const beef = new Beef("和牛タン", 3000); //このメソッドの戻り値が代入される
const many = beef.many();
console.log(many);

子クラスの定義した独自メソッドは、親クラスクラスから呼び出すことはできない

class Beef extends Meat {
  many() {
    return this.price *2;
  }
}

const meat = new Meat("和牛タン", 3000);
const many = meat.many(); //manyメソッドはBeefクラスにしかないのでエラー

オーバーライド

親クラスと同じ名前のメソッドを子クラスに定義すると上書きされ、子クラスのメソッドが優先して使用される

const beef = new Beef("和牛タン", 3000);
beef.info ():
class Meat {
  info() {
    |
  }
}
class Beef extends Meat {
  info() { //子クラスのinfoメソッドが呼び出される
    |
  }
}

コンストラクタのオーバーライド

メソッドと同じように、コンストラクタもオーバーライドすることができる
例えば、子クラスにプロパティを追加したい場合などに用いる
ただし、コンストラクタをオーバーライドするする際は、1行目に「super( )」と記述する

class 親クラス {
  constructor() {
    |
  }
}
class 子クラス extends 親クラス {
  constructor {
    super() //一行目に super()が必要
    //子クラスのコンストラクタの処理
  }
}

子クラスのコンストラクタ内の「super( )」では、
その部分で親クラスのコンストラクタを呼び出している
そのため、親クラスのコンストラクタが引数を受け取る場合には
「super」の後ろの「( )」に引数を渡す必要がある

class Meat {
  constructor(name, age) { //1.親クラスのコンストラクタ実行
    this.name = name;
    this.price = price;
  }
    |
}
class Beef extends Meat {
  constructor(name, price, many) {
    super(name, age);
    this.many = many; //2.子クラス独自の処理実行
  }
    |
}

【複数ファイルの扱い方】

ファイルの分割

コードの量が増えてくると1つのファイルで管理するのが大変になるため
複数のファイルでコードを管理することがある

ファイル分割時のエラー

ファイルを分割したことで、ファイル内に必要な値がなくなるとエラーとなる

export

クラスの定義の後で「export default クラス名」とすることで、
そのクラスをエクスポート(出力)し、他のファイルへ渡すことができる

class Meat {
  |
}
export default Meat; //Meatクラスを他のファイルでも使用できるようにする設定

import

他のファイルで定義されているクラスを使用するにはインポート(読込)をする必要がある
使用するファイルの先頭で「import クラス名 from ". / ファイル名"」と書くことでインポートできる
ファイル名の拡張子の「.js」は省略可

import Meat from "./meat"; //Meatがクラス名、"./meat"がファイル名

値のエクスポート

文字列や数値や関数など、どんな値でもエクスポートが可能
エクスポートする際は、「export default 定数名」とする
インポートする際は、「import 定数名 from "./ファイル名"」とする

<sample1.js>
const text = "Hello Yakiniku";
export default text;
<sample2.js>
import text from "./sample1";
HTMLFormControlsCollection.log(text)

デフォルトエクスポート

export defaultはデフォルトエクスポートを呼ばれ、そのファイルがインポートされると
自動的に「export default 値」の値がインポートされる
そのため「エクスポート時の値の名前と、インポート時の値の名前に違いがあっても問題ない」

<sample1.js>
const meat = new Meat("和牛ホルモン", 1200, 2);
export default meat; //meatがエクスポート
<sample2.js>
import oniku from "./sample1"; //名前が違うが定数meatの値が入る
oniku.info();

名前付きエクスポート

デフォルトエクスポートは値を自動でインポートするため1ファイル1つの値のみ
複数の値をエクスポートしたい場合は、「名前付きエクスポート」を用いる

defaultを書かずに、名前を{ }で囲んでエクスポートする書き方
インポートする値は、エクスポート時と同様に
「import {値の名前} from "./ファイル名"」と{ }で囲んで指定する

また「export {名前1, 名前2}」という形で書くことにより
1つのファイルから複数のエクスポートができる
インポートの際も、コンマで区切ることで複数のインポートができる

<sample1.js>
const meat1 = new Meat("イチボ", 1200, "牛");
const meat2 = new Meat("ぼんじり", 800, "鶏");
export {meat1, meat2};
<sample2.js>
import {meat1, meat2} from "./sample1";
meat1.info();
meat2.info();

【配列を操作するメソッド】

pushメソッド

配列の最後に新しい要素を追加するメソッド
pushメソッドの後の( )の中に追加したい要素を入力する

const meats = ["レバー", "ハツ", "ミノ"];
console.log(meats);
meats.push(シマチョウ); //配列に新しい要素を追加する
console.log(meats);

forEachメソッド

配列の中の要素を1つずつ取り出して、全ての要素に繰り返し同じ処理を行うメソッド

const meats = ["レバー", "ハツ", "ミノ"];
meats.forEach((meats) => {
 console.log(meats);
});

forEachメソッドの引数には「アロー関数」が入っている
配列内の要素が1つずつ順番にアロー関数の引数に代入され、処理が繰り返し実行される

配列 = [要素1, 要素2, 要素3]; //要素1〜3が引数へ
配列.forEach((引数) => {処理}); //(引数) => [処理} の部分がアロー関数
配列の中の要素を1つずつ取り出して同じ処理をする

findメソッド

コールバック関数の処理部分に記述した、
条件式に合う1つ目の要素を配列の中から取り出すメソッド
配列meatsの要素が1つずつ引数meatに代入されて処理が進む
コールバック関数の中は { return 条件 } と書くことで、条件に合う要素が戻り値となる
findメソッドは条件に合う要素が見つかった時に終了するので、条件に合う最初の1つの要素しか取り出せない

const meats = ["レバー", "ハツ", "ミノ"];
const foundMeat = meats.find((meat) => {
  return meat === "ミノ";
});
console.log(foundMeat);

配列の要素がオブジェクトの場合もfindメソッドを使うことができる
以下の例ではオブジェクトのプロパティを条件の中で使用している
オブジェクトのプロパティを条件として使用する場合、そのプロパティを持っているオブジェクトそのものを取り出しす

<script.js>
const meats = [
  {id: 1, name: "ヒレ", price: 2000},
  {id: 2, name: "ミスジ", price: 1500}
];
const foundMeat = meats.find((meat) => {
return meat.id === 1;
});
console.log(foundMeat);

filterメソッド

記述した条件に合う要素のみを取り出して新しい配列を作成するメソッド
下記の例では配列numbersの要素が1つずつ引数numberに代入される
その後、filterメソッド内で「3より大きい数字」かどうかを判定し、
条件に合う要素が定数filteredNumbersに配列として代入される

const numbers = [1, 3, 5, 7];
const filteredNumbers = numbers.filter((number) => {
  return number > 3;
});
console.log(filteredNumbers);
// 条件に合う要素がすべて取り出されて新しい配列の中に入る

findメソッドと同様に、配列の要素がオブジェクトの場合もfilterメソッドを使うことができる
下記の例ではオブジェクトのプロパティを条件の中で使用している
オブジェクトのプロパティを条件として使用する場合、そのプロパティを持っているオブジェクトそのものを取り出す

const meats = [
  {name:"レバー", price: 750},
  {name:"ハツ", price: 850},
  {name:"ミノ", price: 800}
];
const filteredMeats = meats.filter((meat) => {
  return meat.price <= 800 ;
});
  console.log(filteredMeats);

mapメソッド

配列内のすべての要素に処理を行い、その戻り値から新しい配列を作成するメソッド
下記の例では配列numbersの全ての要素を2倍した要素を持つ、新しい配列を作成している
コールバック関数の中の処理は { return 値 } と書く

const numbers = [1, 2, 3];
const doubledNumbers = numbers.map((number) => {
  return number  * 2;
});
console.log(doubledNumbers);
// 全ての数字が2倍された新しい配列が作成され定数doubledNumbersに代入

mapメソッドも配列のオブジェクト要素に対しても使うことができる
以下の例では、mapでfirstNameプロパティとlastNameプロパティを繋げる処理をしている

const names = [
  {firstName: "焼肉", lastName: "吾郎"},
  {firstName: "お肉", lastName: "太郎"}
];
const fullNames = names.map((name) => {
  return name.firstName + name.lastName;
});
console.log(fullNames);

【コールバック関数】

ある他の関数に引数として渡される関数を「コールバック関数」と呼ぶ

umbers.forEach((number) => {console.log(number);});
         ----------------------------------
                       コールバック関数

引数の種類

JavaScriptでは引数に関数を渡すことができる
引数に渡される関数をコールバック関数と呼ぶ

関数呼び出し時に渡される引数の種類
文字列
数値
真偽値
関数 //引数に渡される関数をコールバック関数という

関数の呼び出し方・渡し方

関数名の後ろに( )をつけると呼び出され、( )をつけなければ関数そのものを指す
呼び出し方と渡し方で書き方が異なる

関数の扱い方
kurogeWagyu( )・・・関数が呼び出される
kurogeWagyu・・・関数の定義そのもの

const kurogeWagyu = () => {
  |
};
const call = (callback) => { ・・・関数kurogeWagyuが代入される
  |
};
call(kurogeWagyu); ・・・関数kurogeWagyuを引数に渡す

関数を直接引数の中で定義することもできる

const kurogeWagyu = () => {
  console.log("サーロイン");
};

const call = (callback) => { //2.関数をcallbackに代入
  console.log("コールバック関数を呼び出す");
  callback(); //3.関数callbackを呼び出す
};

call(kurogeWagyu);

call(() => { //1.引数で関数を定義して関数callを呼びだす
  console.log("シャトーブリアン");
});

コールバック関数では、普通の関数と同じように引数を複数渡すことができる

<普通の関数の場合>
const introduce = (name, price) => {
  console.log(`${name}は"{price}円です。`);
};
introduce("シャトーブリアン", 15000);
<コールバック関数の場合>
const introduce = (callback) => {
  callback("シャトーブリアン", 15000);
};
introduce((name, price) => {
  console.log(`${name}は${price}円です。`);
});
// コールバック関数の引数と、実行時に渡す引数の数をそろえるように気を付ける
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リロードしないとJavascriptが動かないバグを修正

はじめに

Rails6.0でアプリを作成していてJavascriptでタブUIの実装をしました。
いざブラウザで確認してみると、該当ページを開いているうちは問題なく動作するのですが
ページ遷移をしてもう一度該当ページに戻ってくると、タブのコンテンツが真っ白になってしまいました。。

その画面でリロードすると正しく動作をするのですが、これではアプリとしてよろしくないので
今回はこのバグの修正の仕方をまとめたいと思います。

目次

1.バグの正体:Turbolinks
2.解決方法

1. バグの正体:Turbolinks

そもそもTurobolinksとは?

Rails4から公式に導入されたGemのひとつです。
簡単に言えば、ウェブアプリケーションをより速く導いてくれる機能が備わっています。

turbolinksでページ遷移するときはページの表示に必要なすべての要素を読み込まずbody要素だけ入れ替えます。しかも入れ替えるときにはjavascriptやjsonを読み込まないでHTMLだけ読み込むのでページ遷移が高速化するという仕組みです。

タイトルにある通り、リロードしないとJSファイルが読み込まれないのは、ページ遷移時にTurbolinksがJSファイルを読み込まないことが原因で起こっていたんですね。

2. 解決方法

解決方法は複数あるようですが、今回はJSファイルにturbolinks:loadというイベントを追加する方法で解決します。このイベントはturbolinksが働くごとにjavascriptを読み込むことを意味します。

test.js
document.addEventListener("turbolinks:load", function() {
  // 処理内容
})

この記述をすることでページが遷移しても本来JSファイルを無視するTurbolinksがうまくJSファイルを読み込んでくれるようになります!

おわりに

最後まで読んでいただきありがとうございました!
お疲れさまでした。。

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

Vue.js 3 入門 「ルーティング」(Vue Router)

はじめに

Vue.js 3 の ルーティング について、自分が学んだことを備忘録として記載します。
Vue.js に殆ど触れたことが無い方に少しでも参考になれば幸いです。
誤り等あれば、ご指摘頂けますと大変喜びます

プロジェクトの作成

まずは Vue CLI を用いてプロジェクトを作成します。
Vue CLI についてはこちらの記事を参照してください。

プロジェクトを作成するには、作成したいフォルダで以下のコマンドを実行します。
hello-routerはプロジェクト名です。任意のプロジェクト名を設定してください。

cd 任意のフォルダ
vue create hello-router

プリセットの選択

すると、以下のように利用するプリセット(プロジェクト設定)の選択を求められます。
まずは最低限の構成とするので「Manually select features」(手動で選択)を選択します。
versionはご自身のバージョンに読み替えてください。

Vue CLI v4.5.9
? Please pick a preset:
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
> Manually select features    

プロジェクトに組み込むモジュールを選択

プロジェクトに組み込むモジュールを選択します。
ここでBabelLinterに加えて、Routerを選択します。
[Space]キーで選択することができ、[Enter]キーで確定となります。

Routerを選択することによって、Vue Routerというルーティング機能を提供するライブラリが組み込まれます。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project:
 (*) Choose Vue version
 (*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
>(*) Router
 ( ) Vuex
 ( ) CSS Pre-processors
 (*) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing                                                                                                                                                                                                                                                                          

Vue.js のバージョンを選択

Vue.js のバージョンを選択します。
本記事では 3.x を選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with
  2.x
> 3.x (Preview)                                                                                                                                                                                              

Historyモードの有効/無効を選択

Historyモードの有効/無効を選択します。
基本的にY(有効)で問題ありません。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) Y   

Linter の設定を選択

Linter の設定を選択します。
今回は最低限のESLint with error prevention only(エラー防止のみ)を選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: (Use arrow keys)
> ESLint with error prevention only
  ESLint + Airbnb config
  ESLint + Standard config
  ESLint + Prettier                                                                                                                                                                                                                                                                                                          

続けて、Lintの実行タイミングの選択を求められます。
Lint on save(保存時)を選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Lint on save
 ( ) Lint and fix on commit      

設定情報の格納先を選択

BabelESLintの設定情報を個別の設定ファイルとするか、package.jsonにまとめるかを選択します。
個別の設定ファイルとしたほうが綺麗なのでIn dedicated config filesを選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
> In dedicated config files
  In package.json 

今回の設定を保存しておくかを選択

今回の設定を保存しておくかを選択します。
今回はあくまでお試しなのでN(保存しない)とします。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) N                                                                                                                         

プロジェクトの生成開始

ここまでの設定内容を元に、プロジェクトの生成が開始されるので、完了するまで待機します。
正常に完了すると、以下のような文言が表示されます。

Vue CLI v4.5.9
Creating project in 任意のフォルダ\hello-router.
Installing CLI plugins. This might take a while...

途中省略...

Running completion hooks...

Generating README.md...

Successfully created project hello-router.
Get started with the following commands:

 $ cd hello-router
 $ npm run serve

生成されたフォルダを確認

カレントフォルダに、指定したプロジェクト名のフォルダが生成されています。

image.png

アプリの実行

早速実行してみましょう。
上記のプロジェクト生成完了時の文言(Get started with the following commands:)にある通り、以下のコマンドを実行します。
プロジェクトルートに移動して、開発用のサーバーを実行するコマンドです。

cd hello-router
npm run serve

以下のような文言が表示されれば、開発用のサーバーが起動できています。
ブラウザを起動しhttp://localhost:8080にアクセスしてください。

  App running at:

途中省略...

  Note that the development build is not optimized.
  To create a production build, run npm run build.

ページの表示

以下のような画面が表示されれば、プロジェクトの作成は成功です。
開発用サーバーは[Ctrl] + [C]で終了することができます。

image.png

挙動を確認する

ここからルーティングの挙動を確認していきます。
http://localhost:8080にアクセスすると、以下のようにHome画面が表示されます。
つまり、Homeコンポーネント(/src/views/Home.vue)が呼び出されていることがわかります。

image.png

ここでアドレスバーから手入力でURLを変更します。
変更前は以下のようになっているかと思います。

http://localhost:8080

アドレスバーから手入力でURLを以下のように変更してください。
末尾に/aboutを追加します。

http://localhost:8080/about

変更後、Enterを押下すると、以下のようにAbout画面が表示されると思います。
つまり、Aboutコンポーネント(/src/views/About.vue)が呼び出されました。

image.png

ここで生じた2つの疑問について、仕組みを説明していきます。

  • http://localhost:8080にアクセスすると、なぜHomeコンポーネント(/src/views/Home.vue)が呼び出されるのか
  • /aboutを追加すると、なぜAboutコンポーネント(/src/views/About.vue)が呼び出されるのか

ルーティング

クライアントから要求されたURLに応じて、処理を受け渡すコンポーネントを決定する仕組みのことです。
ルーティング機能を提供するライブラリのことをルーターと呼び、Vue.js では標準的なルーターとしてVue Routerが用意されています。

ルーティングの流れ

リクエストのURLをアプリケーションが定義しているルート(Route)と照合します。
一致するルートが見つかった場合は、該当するコンポーネントが呼び出されます。

http://localhost:8080/about

従ってAboutコンポーネントが呼び出されるのは、上記のURLをルートと照合した結果、一致するルートが見つかったからということになります。

ルート

では、ルートはどこでどのように定義されているのでしょうか。
ルーティング情報として/src/route/index.jsに定義されています。

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

まず抑えておきたいのはcreateRouterメソッドによって、ルーティング情報を扱うルーターが生成されている点です。

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

createRouterメソッド

  • history
    • historyモードの基本情報
    • ほぼ定型
      • createWebHistory(process.env.BASE_URL)
  • routes
    • ルート
    • ここではroutes変数の値が割り当てられている

実際のルートが定義されているのは、routes変数です。

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]
  • path
    • リクエストパス
  • name
    • ルートの名前
  • component
    • ルーティングによって呼び出されるコンポーネント

従って、ここでは以下の2つのルートが定義されていることになります。

  • Homeルート
    /Homeコンポーネントが呼び出される

  • Aboutルート
    /aboutAboutコンポーネントが呼び出される

結果として、http://localhost:8080にアクセスするとHomeコンポーネントが呼び出され、/aboutを追加すると、Aboutコンポーネントが呼び出されることになります。

ルーターの有効化

なお、ルーターは/src/main.jsで有効化されています。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

Vueインスタンスにライブラリを組み込むuseメソッドに、定義したルーター(router)を渡すことで有効化しています。

ルートを追加してみる

コンポーネントを追加

src/views/Article.vueを追加

src/viewsフォルダには、ルーティングに関わるコンポーネントを配置します。
src/componentsフォルダも同様の役割ですが、こちらにはより細かい部品を配置します。

<template>
    <div class="article">
        <h1>This is an article page</h1>
    </div>
</template>

ルートを追加

src/router/index.jsを以下のように修正

Articleルートを追加します。

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  // ↓ 追加
  {
    path: '/article',
    name: 'Article',
    component: () => import('../views/Article.vue')
  }
]

ついでにTOPのリンクも追加

src/App.vueを以下のように修正

ルーター経由のリンクは、aタグではなく、router-linkを利用します(to属性でリンク先を指定)。
また、ルーター経由で呼び出されたコンポーネントは、router-viewの領域に反映されます。
※ルーターを利用する場合、router-viewによる表示領域の確保は必須

<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link> |
    <router-link to="/article">Article</router-link> <!-- 追加 -->
  </div>
  <router-view/>
</template>

結果を確認

以下のURLにアクセス

http://localhost:8080

Articleページへのリンクが追加されています。

image.png

以下のURLにアクセス または Articleのリンクを押下

http://localhost:8080/article

Articleコンポーネントが呼び出されていることが確認できました。

image.png

以上となります。
ありがとうございました。

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

kintone APIの一括処理を、bulkRequestとPromise.allSettledで堅牢に

はじめに

みなさん、kintoneの bulkRequest(バルクリクエスト)使ってますか?
恥ずかしながら、僕は今まであまり使ってなかったのですが、
今年ようやく「これ超いいやん!」と実感する機会が多くありました。

bulkRequestを解説した記事が、そもそもネット上に少ないですよね。
developer network内に良い記事が2本あるのですが、
Qiitaや個人ブログでは、ここまでちゃんとした記事は見つかりませんでした。
この2本だけでは人の目に触れる機会も少なく、勿体ないです。

多分、存在をそもそも知らない人が多いんじゃないだろうか。
@kintone/rest-api-client の allXXX系メソッドなどで内部的に使われてるので、
SDK経由で間接的に叩いている人は多そうですけどもね。

なので今回はdeveloper networkの記事と多少内容が被ることも覚悟のうえで、
「もっとbulkRequest使っていこうぜ!」という啓蒙の意味を込めて書いてみたいと思います。
ついでに、ES2020から新たにJavaScriptに加わった関数
Promise.allSettled()も一緒に解説します。
この2つをセットで使うと、安定した一括処理を実現しやすくてお勧めですよ!

サンプルアプリ概要

シンプルな「売上」「請求」の2アプリを考えてみます。
image.png

売上アプリ

  • 売上日、顧客名、商品名、単価、数量 を入力する
  • 金額は自動計算
  • 請求状況は自動設定されるので編集不要(本当はdisabled制約あった方が良い)
  • マスタ系、消費税、納品管理などは無視

データは何でもいいんですが、「お弁当屋さん」ぽくしてみました。
image.png
image.png

請求アプリ

  • 一覧画面にボタンを配置する
  • クリックすると以下の処理が走る
    • 「今月の未請求の売上」を売上アプリから抽出
    • 顧客別に集計して請求アプリにレコード追加
    • 売上アプリの請求状況フィールドを「請求済」に更新

image.png

JavaScriptカスタマイズの前提

  • webpackは不要(もちろん使ってもOK)
  • IE対応はしない(必要ならBabelで)
  • 100件越えの一括追加、一括更新は想定しない(改良すれば対応可能)
  • kintone REST APIは @kintone/rest-api-client 経由で叩く
  • 日付の処理は Luxon を使用
  • CSSとして 51-modern-default.css を使用

設定画面はこんな感じになります。
image.png

以下、自作のbilling.jsについて解説します。

カスタマイズ例

ありがちなコード

まずは全体を載せます。こいつを少しずつ改良していきます。

const SALES_APP_ID = 1234

// 顧客ごとの金額を合計
const sumAmountByCustomers = orderRecords => {
  const result = {}
  for (const record of orderRecords) {
    const customer = record.顧客名.value
    if (!result[customer]) {
      result[customer] = 0
    }
    result[customer] += Number(record.金額.value)
  }
  return result
}

// 請求アプリに追加するレコードオブジェクト生成
const generateBillingRecords = orderRecords => {
  const amountByCustomers = sumAmountByCustomers(orderRecords)
  return Object.entries(amountByCustomers).map(([customerName, amount]) => ({
    顧客名: { value: customerName },
    税別金額: { value: amount },
    請求日: { value: luxon.DateTime.local().toISODate() },
  }))
}

// 売上アプリを更新するレコードオブジェクト生成
const generateOrderRecordsAsBilled = orderRecords =>
  orderRecords.map(record => ({
    id: record.$id.value,
    revision: record.$revision.value,
    record: { 請求状況: { value: '請求済' } },
  }))

// メイン
const onClickBillingButton = async () => {
  if (!confirm('請求レコードを一括作成します。よろしいですか?')) return

  try {
    const client = new KintoneRestAPIClient()
    const orderRecords = await client.record.getAllRecords({
      app: SALES_APP_ID,
      condition: '売上日 = THIS_MONTH() and 請求状況 in ("未請求")',
    })
    if (orderRecords.length !== 0) {
      alert('請求対象の売上が見つかりません。')
      return
    }

    await client.record.addRecords({
      app: kintone.app.getId(),
      records: generateBillingRecords(orderRecords),
    })
    await client.record.updateRecords({
      app: SALES_APP_ID,
      records: generateOrderRecordsAsBilled(orderRecords),
    })

    alert('完了しました')
    location.reload()
  } catch (e) {
    console.error(e)
    alert('エラーが発生しました。')
  }
}

// イベントハンドラ、ボタン配置処理
kintone.events.on('app.record.index.show', event => {
  if (document.querySelector('#billing-button')) return
  const button = document.createElement('button')
  button.id = 'billing-button'
  button.className = 'kintoneplugin-button-dialog-ok'
  button.innerText = '請求レコード作成'
  button.onclick = onClickBillingButton
  kintone.app.getHeaderMenuSpaceElement().appendChild(button)
})

全体を即時関数で括るのは省略してます。本当はこうやってね。

(() => {
  const SALES_APP_ID = 1234
  // 以下略
})()

version-1.0

全体の中で、この部分に注目。
「請求レコード追加」からの「売上レコード更新」をしている箇所です。
addRecords getRecordsを、それぞれawaitしてます。

    await client.record.addRecords({
      app: kintone.app.getId(),
      records: generateBillingRecords(orderRecords),
    })
    await client.record.updateRecords({
      app: SALES_APP_ID,
      records: generateOrderRecordsAsBilled(orderRecords),
    })
    alert('完了しました')

以降では上記の処理に絞って、
少しずつ改善しながらbulkRequest allSettledのメリットを解説していきます。

version-1.0の問題点

この記事で一貫しているポイントは、
「途中で1か所エラーが起きた時、他にどんな影響が及ぶか?」です。

kintoneでは、APIで複数レコードの一括登録・更新を行う場合、
途中でエラーが起きると、そのAPI一発分はすべてロールバックしてくれます。
version-1.0のコードでは、こんな感じですね。

  • 請求レコード追加時にエラーが起きると、全部のレコード追加をロールバック
  • 売上レコード更新時にエラーが起きると、全部のレコード更新をロールバック

さて、今回はそれぞれを順番にawaitしているわけですが、ここに落とし穴があります。
以下のようになった場合、全体として何が起きるでしょう?

  • 請求レコード追加は全件成功
  • 売上レコード更新の途中でエラー発生→ロールバック

これだと、「実際には請求レコードが追加されているのに、請求状況はすべて未請求」
というチグハグな状態が起きてしまいます。これはよろしくない。

version-1.1

1.0の問題点を解決する前に、
JSのPromiseに慣れている人がやりがちな別問題にも触れておきます。

初心者は「非同期処理に何でもawaitつける」のをやりがちですが、
それやると本来の非同期のメリットがなくなって、パフォーマンスが遅くなります。
中級者以上は、Promise.allで並列処理をすることも多いでしょう。
しかし、、、

    await Promise.all([
      client.record.addRecords({
        app: kintone.app.getId(),
        records: generateBillingRecords(orderRecords),
      }),
      client.record.updateRecords({
        app: SALES_APP_ID,
        records: generateOrderRecordsAsBilled(orderRecords),
      }),
    ])
    alert('完了しました')

version-1.1の問題点

これ、パフォーマンスは速くなりますが、
「エラー時の堅牢さ」という意味ではむしろversion-1.0よりも悪化してます。

Promise.all()は「最初にエラーが起きた時点で全部止まる」ように見えるものの、
実際には「裏で残りのPromiseは動き続けて、それらの成功・失敗は把握できない」のです。
一度走り出したPromiseは、途中キャンセルされないので、覚えておきましょう。

てことは、1.1のコードの場合、

  • 請求レコード追加が失敗した
    • けど、売上レコードの更新は成功したかもしれない
    • もしくは、売上レコードの更新も失敗したかもしれない
  • 売上レコード更新が失敗した
    • けど、請求レコードの追加は成功したかもしれない
    • もしくは、請求レコードの追加も失敗したかもしれない

最初に発生したエラー以外は何も把握できないので、
「かもしれない」部分は、実際のレコード値を自分で確認するしかないのです。
その点 version-1.0 は、「確実にこれ以降が失敗した」とわかるので、まだマシなんですね。

Promise.all()は「レコード取得系の処理」で使う分には大変便利なのですが、
追加系・更新系では使わない方が安全です。

では、どうすればいいか?
いよいよbulkRequestの登場です!

version-2.0

こんな感じでbulkRequestを送ることで、
「請求レコード追加」「売上レコード更新」をセットで一括処理することができます。

    await client.bulkRequest({
      requests: [
        {
          method: 'POST',
          api: '/k/v1/records.json',
          payload: {
            app: kintone.app.getId(),
            records: generateBillingRecords(orderRecords),
          },
        },
        {
          method: 'PUT',
          api: '/k/v1/records.json',
          payload: {
            app: SALES_APP_ID,
            records: generateOrderRecordsAsBilled(orderRecords),
          },
        },
      ],
    })
    alert('完了しました')

Promise.all()では「一度走り出したPromiseは、途中キャンセルされない」のですが、
bulkRequestを使うと、「並列処理の1つでも失敗すると、すべてキャンセル(ロールバック)」してくれるのが素晴らしいところです。
どこかでエラーが起きても、こんな感じで確実に全部ロールバックされます。

  • 請求レコード追加が失敗した
    • 売上レコード更新もすべてキャンセル
  • 売上レコード更新が失敗した
    • 請求レコード追加のすべてキャンセル

version-2.0の問題点

「エラー時に中途半端な状態にならない」という意味では問題なくなりましたが、
あとは「1つでもエラーが起きたら全部キャンセルされる」という部分ですね。

100件処理するときに、1件だけおかしいのに99件も全部止まったら悲しいじゃないですか。
そこを何とかしたい。

あと、本題とは逸れますが、
rest-api-clientbulkRequestを送るには、リクエストボディは生で書く必要があるんですよねー。これがイマイチ。
以前のkintone-js-sdkでは、メソッドチェーンでbulkRequestを送ることができたので良かったんですけどね。退化しちゃいましたね。今後に期待!
GitHubにはIssue送り済みですw https://github.com/kintone/js-sdk-ja/issues/12

version-3.0

1.0 -> 1.1 -> 2.0と「APIの叩き方」を変えてきましたが、
3.0では「リクエストボディの作り方」に手を加えます。

    const billingRecords = generateBillingRecords(orderRecords)
    const recordsForBulkRequest = billingRecords.map(billingRecord => {
      const orderRecordsOfThisCustomer = orderRecords.filter(
        orderRecord => orderRecord.顧客名.value === billingRecord.顧客名.value
      )
      const orderRecordsAsBilled = generateOrderRecordsAsBilled(orderRecordsOfThisCustomer)
      // 顧客別に、請求追加用(1件) / 売上更新用(複数件)をペアにする
      return { billingRecord, orderRecordsAsBilled }
    })

    for (const { billingRecord, orderRecordsAsBilled } of recordsForBulkRequest) {
      await client.bulkRequest({
        requests: [
          {
            method: 'POST',
            api: '/k/v1/record.json',
            payload: {
              app: kintone.app.getId(),
              record: billingRecord,
            },
          },
          {
            method: 'PUT',
            api: '/k/v1/records.json',
            payload: {
              app: SALES_APP_ID,
              records: orderRecordsAsBilled,
            },
          },
        ],
      })
    }
    alert('完了しました')

リクエストボディの分け方を簡略化して比べると、こんなイメージ

before
{
  "請求": [{}, {}],
  "売上": [{}, {}, {}, {}, {}]
}
after
[
  {
    // A社への請求
    "請求": {},
    "売上": [{}, {}, {}]
  },
  {
    // B社への請求
    "請求": {},
    "売上": [{}, {}]
  }
]

「アプリ単位」ではなくて、「トランザクション単位」とでも言いますか。
エラーが起きた時に、この単位でロールバックしたいという塊にしておきます。

その上で、for-ofでループさせて一件ずつ処理をしています。
これなら途中でエラーが起きても、それ以前に成功していたリクエストは、
無駄にロールバックされることなく生き残ってくれます。

version-3.0の問題点

例えば10件中の6件目でエラーが起きた場合、こうなります。

  • 前半5件は処理成功のまま残る
  • エラーが起きた6件目はロールバック
  • 後半4件は全く処理されない(try-catchで抜けてしまう)

version-2.0よりはマシになりましたが、もう一声。
ぜひとも「後半の4件も」無視せずに救いたいですよね。

さて、いよいよ次が最終形態、Promise.allSettled()の登場です。

version-4.0

3.0では「forの中で1件ずつawait」という、素人っぽい方法を使っちまいましたw

最終形態4.0では、ループ内ではawaitせずに、pending状態のPromiseオブジェクトを
ひたすら配列に突っ込んでいき、ループを回しきる。
そして、一番最後にawait Promise.allSettled()で待機します。

    const promises = []
    for (const { billingRecord, orderRecordsAsBilled } of recordsForBulkRequest) {
      // ここではawaitせず、pending状態で突っ込むだけ
      const promise = client.bulkRequest({
        requests: [
          {
            method: 'POST',
            api: '/k/v1/record.json',
            payload: {
              app: kintone.app.getId(),
              record: billingRecord,
            },
          },
          {
            method: 'PUT',
            api: '/k/v1/records.json',
            payload: {
              app: SALES_APP_ID,
              records: orderRecordsAsBilled,
            },
          },
        ],
      })
      promises.push(promise)
    }
    // 失敗が混ざってもキャンセルせず、すべての実行結果を取得
    const results = await Promise.allSettled(promises)

    // 成功・失敗それぞれを抽出
    const successes = results.filter(_ => _.status === 'fulfilled')
    const errors = results.filter(_ => _.status === 'rejected')

    alert(`請求レコード作成 実行結果

請求対象顧客:${results.length}件
処理成功:${successes.length}件
処理失敗:${errors.length}`)

    if (errors.length > 0) {
      console.error(errors)
      return
    }

結果、こちら!
テストデータ手を抜いて少ないですが、「成功・失敗」の件数が全部出ます!

image.png

Promise.all()の問題点が、
Promise.allSettled()を使うことで見事に解決されました!

settledってのはfulfilled(成功) rejected(失敗)が混在する総称。
「成功・失敗どちらにしろ全部終わるまで待機」ってことですね。
詳しくはこの記事あたりが参考になります。

allSettled()全体としては、エラーが起きてもtry-catchではキャッチされません。
必ず成功するので、玉石混合の実行結果を自分でフィルタして、successes errorsに分けてあげます。

あとはalertでいい感じにメッセージ出してあげればOK。
ちゃんとやるならSweetAlertなんか使って、
エラーオブジェクトの中身も奇麗に表示してあげると良いでしょう。

改善後 全コード

最後に、改めて最終形態のコード全体を載せておきます。

const SALES_APP_ID = 1234

// 顧客ごとの金額を合計
const sumAmountByCustomers = orderRecords => {
  const result = {}
  for (const record of orderRecords) {
    const customer = record.顧客名.value
    if (!result[customer]) {
      result[customer] = 0
    }
    result[customer] += Number(record.金額.value)
  }
  return result
}

// 請求アプリに追加するレコードオブジェクト生成
const generateBillingRecords = orderRecords => {
  const amountByCustomers = sumAmountByCustomers(orderRecords)
  return Object.entries(amountByCustomers).map(([customerName, amount]) => ({
    顧客名: { value: customerName },
    税別金額: { value: amount },
    請求日: { value: luxon.DateTime.local().toISODate() },
  }))
}

// 売上アプリを更新するレコードオブジェクト生成
const generateOrderRecordsAsBilled = orderRecords =>
  orderRecords.map(record => ({
    id: record.$id.value,
    revision: record.$revision.value,
    record: { 請求状況: { value: '請求済' } },
  }))

// メイン
const onClickBillingButton = async () => {
  if (!confirm('請求レコードを一括作成します。よろしいですか?')) return

  try {
    const client = new KintoneRestAPIClient()
    const orderRecords = await client.record.getAllRecords({
      app: SALES_APP_ID,
      condition: '売上日 = THIS_MONTH() and 請求状況 in ("未請求")',
    })
    if (orderRecords.length === 0) {
      alert('請求対象の売上が見つかりません。')
      return
    }

    const billingRecords = generateBillingRecords(orderRecords)
    const recordsForBulkRequest = billingRecords.map(billingRecord => {
      const orderRecordsOfThisCustomer = orderRecords.filter(
        orderRecord => orderRecord.顧客名.value === billingRecord.顧客名.value
      )
      const orderRecordsAsBilled = generateOrderRecordsAsBilled(orderRecordsOfThisCustomer)
      // 顧客別に、請求追加用(1件) / 売上更新用(複数件)をペアにする
      return { billingRecord, orderRecordsAsBilled }
    })

    const promises = []
    for (const { billingRecord, orderRecordsAsBilled } of recordsForBulkRequest) {
      // ここではawaitせず、pending状態で突っ込むだけ
      const promise = client.bulkRequest({
        requests: [
          {
            method: 'POST',
            api: '/k/v1/record.json',
            payload: {
              app: kintone.app.getId(),
              record: billingRecord,
            },
          },
          {
            method: 'PUT',
            api: '/k/v1/records.json',
            payload: {
              app: SALES_APP_ID,
              records: orderRecordsAsBilled,
            },
          },
        ],
      })
      promises.push(promise)
    }
    // 失敗が混ざってもキャンセルせず、すべての実行結果を取得
    const results = await Promise.allSettled(promises)

    // 成功・失敗それぞれを抽出
    const successes = results.filter(_ => _.status === 'fulfilled')
    const errors = results.filter(_ => _.status === 'rejected')

    alert(`請求レコード作成 実行結果

請求対象顧客:${results.length}件
処理成功:${successes.length}件
処理失敗:${errors.length}`)

    if (errors.length > 0) {
      console.error(errors)
      return
    }

    location.reload()
  } catch (e) {
    console.error(e)
    alert('エラーが発生しました。')
  }
}

kintone.events.on('app.record.index.show', event => {
  if (document.querySelector('#billing-button')) return
  const button = document.createElement('button')
  button.id = 'billing-button'
  button.className = 'kintoneplugin-button-dialog-ok'
  button.innerText = '請求レコード作成'
  button.onclick = onClickBillingButton
  kintone.app.getHeaderMenuSpaceElement().appendChild(button)
})

おわりに

ステップ・バイ・ステップで、少しずつコードを改善して、
一括処理を安定化させてきました。いかがだったでしょうか?

RDBのトランザクションに比べるとkintoneは頼りない部分も多いですが、
それでも、ここまで出来ればかなり幅広い用途に耐えられるのではないかと思っとります。
bulkRequestPromise.allSettled、とても便利なのでぜひ使いこなしてみてください。

アドベントカレンダーでは、ネタ系の楽しい投稿がとても多いですよね。
僕もいつも楽しく読ませてもらってます。
言ってみれば「J-POP」であります :guitar:

しかしながら、自分が書く記事はやはり
「こうすれば今後の開発にとっても役に立つよ!」という実用的でガチなネタを
今後も中心にしたいと思っております。
言ってみれば「演歌」ですね :microphone:

kintone演歌歌手 @the_red の次回作に、今後もご期待ください :smile:
それでは、みなさま良いクリスマスを~ :christmas_tree:

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

React Hook Formを初めて使う人へ

開発のプロジェクトで「ReactHookFormで実装して」と言われて触る機会があり,初心者なりに何度か詰まってから理解したので,つまらない用に超絶簡易ドキュメントを書く.

React Hook Formとは

Home

特徴

  • onChangeを走らせずにすみパフォーマンスがつおい
  • inputやselectのvalueをsubmitのときに拾ってくれる

多分他にもっとある


install

npm

npm install react-hook-form

yarn

yarn add react-hook-form

普通に使う

公式から持ってきたサンプルを説明します.

import React from "react"
import { useForm } from "react-hook-form"

export default function App() {
  const { register, handleSubmit, watch, errors } = useForm()
  const onSubmit = data => console.log(data)

  console.log(watch("example")); // watch input value by passing the name of it

  return (
    {/* "handleSubmit" will validate your inputs before invoking "onSubmit" */}
    <form onSubmit={handleSubmit(onSubmit)}>
    {/* register your input into the hook by invoking the "register" function */}
      <input name="example" defaultValue="test" ref={register} />

      {/* include validation with required or other standard HTML validation rules */}
      <input name="exampleRequired" ref={register({ required: true })} />
      {/* errors will return when field validation fails  */}
      {errors.exampleRequired && <span>This field is required</span>}

      <input type="submit" />
    </form>
  );
}

useFormをimportする

import { useForm } from "react-hook-form"

必要なものを分割代入して使う

const { register, handleSubmit, watch, errors } = useForm()

最低限registerhandleSubmitは必要になるかな

監視対象にregisterを設定

<input name="example" defaultValue="test" ref={register} />

ref属性がregisterがであるものを監視

registerは以下のように引数にオブジェクトを渡すことで,バリデーションの設定ができる

ref={register({ required: true })}

その他の詳細設定はここを参照↓

https://react-hook-form.com/jp/api#register

送信の設定

<form onSubmit={handleSubmit(onSubmit)}>

formタグのonSubmit属性にhandleSubmit()を設定.この引数にフォームを送信する関数を渡す

今回は,const onSubmit = data => console.log(data)が実行される

仕組みは,refにregisterが設定された要素のvalueが,その要素のnameがプロパティとなり,dataにオブジェクトとして渡される.

今回であれば,

data = {example: "入力されたvalue",exampleRequired: "入力されたvalue"}と言った具合

バリデーションのエラーメッセージ

registerに関するエラーは,

const { register, handleSubmit, watch, errors } = useForm()

で定義したerrorsに返ってくるので

errors.exampleRequiredとすれば呼び出せる


外部ライブラリに対応した使い方

さて,便利なHookFormだが,Material-UIなどのツールと共存すると正常に動作しないので困る.

ここをよくわかっていないと,「Material-UIと共存できないとかwww 使う意味なくね?wwww」と言われる.

制御ができない場合はControllerコンポーネントを使って制御する

制御ができない場合とはref属性を設定することができないもののようです.

Controllerをimportする

import { useForm, Controller } from "react-hook-form";

もちろんuseFormも忘れずに

controlを定義しておく

const { handleSubmit, register, errors, control } = useForm();

こんな感じに,普通に使い場合に加えて,Controllerコンポーネントとcontrolを準備する.

外部ライブラリのコンポーネントを制御する書き方

今回のサンプルはみんな大好きMaterialUI

<Controller
  name="gender"
  // control属性忘れずに
  control={control}  
  // component
  as={
    <RadioGroup aria-label="gender" name="gender1" >
      <FormControlLabel value="female" control={<Radio />} label="Female" />
      <FormControlLabel value="male" control={<Radio />} label="Male" />
      <FormControlLabel value="other" control={<Radio />} label="Other" />
      <FormControlLabel value="disabled" disabled control={<Radio />} label="(Disabled option)" />
    </RadioGroup>
  // validation
    rules={{ required: true }}
  }
/>

オブジェクトのプロパティとなるname属性をセットしましょう

name="gender"

先程定義したcontrol属性もこんな感じにセットします

control={control}

そして制御したいコンポーネントをasに入れます.

今回はMaterialUIのラジオボタンです.

また,このControllerコンポーネントを使用した場合はregisterを使わないので,バリデーションどーすんの?となる.この場合はrules属性を使う.ここがドキュメントちゃんと読んでいなかったので詰まった?

rules={{ required: true }}

こんな感じでOK

欠点

TypeScriptとの相性がわるい(かもしれない)

試してはないし,よくわかっていない?

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

Chime SDKを使ってZoomライクなアプリを作ろうとしたら意外と苦戦した話

概要

この記事は CA21 Advent Calendar 2020 Day16の記事です。

タイトルの通り、Chime SDKを用いてZoomっぽいアプリを作ろうとしたらハマりどころが結構多くて苦戦したお話です。

本記事ではChime SDKについて、以下のことを説明します。

  1. Chime SDKとは
  2. 実装時に役に立つドキュメント
  3. 実装時にハマるポイント
  4. 通話アプリの実装にChime SDKを使うべきかどうか

Amazon Chime SDKを使うことになった経緯

インターン先で開発しているサービスで、これまでZoomを利用していた通話機能部分を内製化する要件が発生しました。
最初はZoom SDKを使おうとしていましたが、UI変更が困難であること, ZOOMに依存した構造になってしまうこと(たとえば使用に際してZoomのアカウントが必要となること)を懸念し、 UIの拡張性の高いChime SDKの使用を決めました。
スクリーンショット 2020-12-15 14.51.04.png

Amazon Chime SDKについて

Amazon Chime SDKとは

簡単に言えば「通話アプリに必要な裏側の基盤をまるっとサポートしてくれる開発キット」です。
通常zoomのような通話アプリを作ろうとすると、WebRTCなど特殊な技術の勉強が必要となります。また、人数が増えた時の拡張性などを考慮したサーバー構築なども容易ではありません。
Chime SDKを使えばそういった煩雑な構築を完全にAWSに委ねることができます。また逆にUIに関しては余計なサポートがないため, 自分の思い通りのUIを作ることも容易です

Amazon Chime SDKを用いた通話アプリの構築

本記事では構築に関しては詳しく言及しませんが、以下記事が非常に参考になりました

amazon-chime-sdk-js(公式)
使用ケースごとにサンプルコードを書いてくれていてかなり親切. ここを見ればたいていの機能は構築できる。
Amazon Chime SDKを試してみた
日本語の記事の中では構築方法についてしっかり解説してくれている良記事。

はまった点

このように便利に思えるChime SDKですが、実際に開発に使ってみるとかなりはまりどころがありました。本記事では自分がはまった問題点とその解決策を紹介します。

部屋の保存時間が限られる

開発中に「一度作ったはずの部屋が一定時間後に消えてしまう(アクセスできなくなる)」という事象が発生しました。
サポートに問い合わせてみたところ、以下の条件を満たすと、作成したmeeting roomが自動削除されてしまうことがわかりました

1. 誰もAudio接続しないまま5分が経過した場合
2. 1人のみが接続した状態で30分以上が経過した場合
3. 1人のみがscreen shareをした状態で30分以上が経過した場合
4. meeting開始から24時間がたった場合

すなわち、Chime SDKではZoomのような部屋の作り置きができないということです。
これを解決するには、以下図のように、前もって別のDBにmeeting情報を保存しておき、開始時に初めてChimeでroom作成する、などの工夫が必要です。
スクリーンショット 2020-12-15 15.05.07.png

SSL化しないとうまく動かない

デプロイ後、以下のコードで本番環境のみエラーが発生する事象に遭遇しました。

//videoデバイスの設定
this.videoInputDevices = await meetingSession.audioVideo.listVideoInputDevices();

こちらもサポートに問い合わせてみたところ、Chime SDKを用いたアプリはhttpsプロトクルを用いた環境でないとうまく動作しないということがわかりました。
(上記コードが内部的にnavigator.mediaDevicesを呼び出しており、httpプロトコル化ではこれが未定義になるのが原因らしい)
だが、なぜlocalhostだと動いてしまうのかは謎...教えて強い人...!

自分以外の参加者の「ビデオオフ」と「退出」の判別ができない

Chime SDKでは参加者の画面On Offの検知をするために以下のようなobserverを定義します。

const observer = {
        videoTileDidUpdate: async (tileState) => {
          //videoをつけた時の処理
        },
        videoTileWasRemoved: (tileId) => {
          //videoを消した時の処理
        }
      }
meetingSession.audioVideo.addObserver(observer)

ところが上記observer内のvideoTileWasRemovedは該当の参加者が退出した際にも、ビデオをオフにした際にも反応してしまいます。すなわちobserver単体だと、退出とビデオオフの状態区別ができないのです...(この仕様のせいか、Amazon Chimeの公式アプリは、カメラをオフ専用のUIが用意されていません)

これを解決するためには、退出検知用に以下のようなコードを書く必要があります。

      //入退出処理
      const callback = async (presentAttendeeId, present) => {
        if (present) {
         //入室時処理
        } else {
          //退出処理
        }
      }
      meetingSession.audioVideo.realtimeSubscribeToAttendeeIdPresence(
        callback
      )

このコードは参加者の入退室時のみ駆動し、定義した動作を行ってくれます。
つまり、observerのvideoTileWasRemovedにひっかかったときは問答無用で画面オフとみなす, 退出検知のコードに引っかかった時は退出とみなすという区別ができるわけですね。(画面オフ専用のobserverもできればつくってほしいけど...)

録画の仕方が特殊

ここが現状最大のWeak Pointだと思うのですが、Chime SDKは画面および音声の録画機能を標準サポートしていません。

代替策として、公式が録画APIのデモを提供しているのでこちらを利用することになります。
ただ、この録画APIの仕組みは「指定したURLの画面および音声をキャプチャする」ことですので、実際の通話アプリとは別に録画用のページを用意する必要があります。

余談ですが、このデモではec2のt3.xlargeが2台たちあがります。放置しているとお財布にかなりの打撃が入るので、READMEにも書いている通り、使用後は必ず全て削除するようにしましょう。 (私はここに1ヶ月気づかず数万円ふっとばしました :moneybag:

結論 「通話アプリにAmazon Chimeを使うのは有効か?」

なんだかんだと文句は書いてきましたが、基本的には工夫すれば通話アプリに必要なほとんどの機能は実装できるため、かなりつかえるSDKだと感じました。
特に以下のような場合はまよわずChime SDKを使っていいのではないかと思います。

  1. 特殊なUIの通話アプリを作りたい(UIのカスタマイズが自由なSDKを使いたい)
  2. バックエンドの仕様やスペックは丸投げでいい(むしろまるなげしたい)
  3. 他のアプリに依存しない通話アプリを作りたい

コロナの影響で、通話アプリdeveloperの需要が高まると考えられる今日この頃、皆様も是非一度試してみてはいかがでしょうか?

PR

内定先のCyberAgentでは22卒のエンジニア採用を行っています!
https://www.cyberagent.co.jp/careers/news/detail/id=25511
皆様のご応募お待ちしております!

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

【重要】非同期通信でOptimizeを使いA/Bテストをする方法

こんにちは、シンヤです!
本日は「【重要】非同期通信でOptimizeを使いA/Bテストをする方法」というテーマでお話しいたします。


概要

非同期通信の場合Google OptimizeでA/Bテストを行うには、以下のような特殊な設定が必要です。

  1. 「アクティベーションイベント」を設定する
  2. テスト有効化のタイミングを「アクティベーションイベント発火後」に設定する

React製のフレームワーク「Next.js」を使ってOptimizeの設定を書き込んでいきます。

Google Optimizeとは

Google Optimizeとは、Googleが提供しているA/Bテストを行えるツールです。

主な特徴として・・・

  • 無料
  • Google Analyticsとの連携機能
  • 過去の測定データの蓄積と分析
  • 「多変量テスト」「リダイレクトテスト」も可能
  • 簡単にデザインとHTMLを変更できる

があります。

どのテストも行いたいことは、基本的には「仮説検証」です。
考え出した仮説を基にGoogle Optimizeを使い、画面の見た目を変更して、2週間〜1ヶ月程テストします。


やること整理

環境構築〜テスト完了まで、全て解説します。
具体的には以下の順番で解説します。

  1. 「Node.js」のインストール
  2. 「Next.js」のプロジェクト作成
  3. 「_document.js」の設定とテストページの作成
  4. 「アクティベーションイベント」の書き込み
  5. 「Google Analytics(GA4)」のアカウント作成
  6. 「Google Optimize」のアカウント作成
  7. 「Google Analytics」「Google Optimize」の連結
  8. 「Next.js」にGAとOptimizeのIDを書き込む
  9. 「Netlify」の設定
  10. 「Google Optimize」でのテスト

1. 「Node.js」のインストール

以下はMacでの環境構築の方法となります。

ご使用中のMacに「Node.js」の環境を構築していきます。
既に環境構築済みの方は、こちらの項目は飛ばして読んでください。

ターミナルを起動する

まずは、ターミナルの起動を行います。
Mac下部の「Launchpad」で、「terminal」と入力して出てきたアイコンを、クリックしてください。

Homebrewのインストール

以下のコマンドを入力して、Homebrewをインストールします。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

インストール後以下のコマンドを入力して、Homebrewのバージョンが表示されたら、成功しています。

$ brew -v
#> Homebrew 1.2.3
#> Homebrew/homebrew-core (git revision 7212; last commit 2017-07-02)

Nodebrewのインストール

以下のコマンドを入力して、Nodebrewをインストールします。

$ brew install nodebrew

インストール後、以下のコマンドを入力して、Nodebrewのバージョンが表示されたら、成功しています。

$ nodebrew -v
#> nodebrew 1.0.0

Nodebrewを使える様にする

以下のコマンドをターミナル上で入力して、環境変数を追加します。

$ vi ~/.bash_profile

ターミナルの画面が変わったら、

  1. キーボードの「I」を押す
  2. 「export PATH=$HOME/.nodebrew/current/bin:$PATH」のコードを追加する。
上記の画像のようになっていれば成功です。

コードを追加したら、

  1. 「esc」キーを押す
  2. 「:wq」と入力する
  3. 「Enter」を押す

コードの保存が終わり、エディターが終了します。

Nodebrewのセットアップ

以下のコマンドを入力して、先ほど作ったbash_profileを反映させます。

$ source ~/.bash_profile

次に以下のコマンドを入力して、Nodebrewをセットアップします。

$ nodebrew setup

Node.jsのインストール

以下のコマンドを入力して「Node.js」をインストールします。

$ nodebrew install-binary v12.13.1

次に以下のコマンドを入力して、インストールした「Node.js」を使える様にします。

$ nodebrew use v12.13.1

インストール後以下のコマンドを入力して、「Node.js」のバージョンが表示されたら、成功しています。

$ node -v
#> v12.13.1

2. 「Next.js」のプロジェクト作成

以下のコマンドを入力して、「Next.js」のプロジェクトを作ります。

$ npx create-next-app
✔ What is your project named? … next-app-optimize

What is your project named?に好きな名前を入れます。
これがディレクトリ名になります。

上記では仮に「next-app-optimize」を名付けました。

上記の画像のようになっていれば成功です。

次に以下のコマンドを入力して、作成した「next-app-optimize」に移動します。

$ cd next-app-optimize

以下のコマンドを入力して、ローカルで「Next.js」を起動させます。

$ yarn dev
実行結果
http://localhost:3000にアクセスして、上記の画面が表示されていれば成功です。

「index.js」を書き換える

pagesディレクトリにあるindex.jsを以下のように書き換えて、不要なものを全て削除します。

import React from "react";
import Head from 'next/head';
import Link from 'next/link';
import styles from '../styles/Home.module.css';

const Home = () => {

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>Top</h1>

        <Link href="/lp">
          <a style={{ textDecoration:'underline' }} className={styles.title}>LPへのリンク</a>
        </Link>
      </main>
    </div>
  )
}

export default Home;

/lpのページはこれから作成します。

上記の画像のようになっていれば成功です。

3. 「_document.js」の設定とテストページの作成

以下のコマンドを入力して、プロジェクトのルートディレクトリにいるか確認します。

$ ls
> README.md node_modules    package.json    pages       public      styles      yarn.lock

pagesディレクトリがあればOKです。
以下のコマンドを入力して、pagesディレクトリに移動します。

$ cd pages

以下のコマンドを入力して、_document.jsファイルを作成します。

$ touch _document.js

作成した_document.jsファイルを、以下のように書き換えます。

import React, { Fragment } from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const isProduction = process.env.NODE_ENV === 'production';

    const initialProps = await Document.getInitialProps(ctx);

    return { ...initialProps, isProduction };
  }

  render() {
    const { isProduction } = this.props;

    return (
      <Html>
        <Head>
          {isProduction && (
            <Fragment>
              <script async src="https://www.googletagmanager.com/gtag/js?id={{AnalyticsのID}}" />
              <script
                dangerouslySetInnerHTML={{
                  __html: `
                  window.dataLayer = window.dataLayer || [];
                  function gtag(){dataLayer.push(arguments);}
                  gtag('js', new Date());
                  gtag('config', '{{AnalyticsのID}}', { 'optimize_id': '{{OptimizeのID}}'});
                `
                }}
              />
            </Fragment>
          )}
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument;

{{AnalyticsのID}}{{OptimizeのID}}は、後ほど書き換えます。

テストページの作成

以下のコマンドを入力して、pagesディレクトリにいるか確認します。

$ ls
_app.js     _document.js    api     index.js

以下のコマンドを入力して、OptimizeでA/Bテストを行うためのページを作成します。

$ touch lp.js

作成したlp.jsファイルを、以下のように書き換えます。

import React from "react";
import Head from 'next/head';
import Link from 'next/link';
import styles from '../styles/Home.module.css';

const lp = () => {

  return (
    <div className={styles.container}>
      <Head>
        <title>LP</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>LP</h1>

        <Link href="/">
          <a style={{ textDecoration:'underline' }} className={styles.title}>Topへのリンク</a>
        </Link>
        <span style={{ background: '#222222', color: '#ffffff', padding: '16px', marginTop: '16px', borderRadius: '8px', cursor: 'pointer' }}>
          LPのボタン
        </span>
      </main>
    </div>
  )
}

export default lp;
上記画像のデザインになっていて、かつトップページからテストページへの遷移が出来る様になっていれば成功です。

4. 「アクティベーションイベント」の書き込み

非同期通信の場合、テスト有効化のタイミングをアクティベーションイベント発火後にする必要があります。
なのでまず、テストページでイベントが発火するように設定します。

dataLayer.push({'event': 'optimize.activate'});

上記のコードをlp.jsに書き込みます。

import React, { useEffect } from "react";
import Head from 'next/head';
import Link from 'next/link';
import styles from '../styles/Home.module.css';

const lp = ( location ) => {

  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({'event': 'optimize.activate'});
  }, [location.pathname])

  return (
    <div className={styles.container}>
      <Head>
        <title>LP</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>LP</h1>

        <Link href="/">
          <a style={{ textDecoration:'underline' }} className={styles.title}>Topへのリンク</a>
        </Link>
        <span style={{ background: '#222222', color: '#ffffff', padding: '16px', marginTop: '16px', borderRadius: '8px', cursor: 'pointer' }}>
          LPのボタン
        </span>
      </main>
    </div>
  )
}

export default lp;

5. 「Google Analytics(GA4)」のアカウント作成

事前にGoogle Analyticsのダッシュボードにログインしてください。

Step1
サイドバーの【管理】をクリックし【アカウント作成】をクリックします。
Step2
「アカウント名」を入力して【次へ】をクリックします。
Step3
【詳細オプションを表示】をクリックします。
Step4
「ユニバーサルアナリティクスプロパティの作成」のトグルをONにします。
URLを入力して【次へ】をクリックします。

ユニバーサルアナリティクスプロパティでないと、Google Optimizeは使えないので、必ずトグルをONにしてください。

Step5
「ビジネスの概要」の入力は任意なので【作成】をクリックします。
Step6
利用規約を読み、チェックを入れて【同意する】をクリックします。

6. 「Google Optimize」のアカウント作成

Step1
Google Optimizeの公式ページにアクセスして【無料で利用する】をクリックします。
Step2
【利用を開始】をクリックします。
Step3
【次へ】をクリックします。
Step4
チェックを入れて【完了】をクリックします。
Step5
【アカウントを作成】をクリックします。
Step6
「アカウント名」を入力し、チェックを入れて【次へ】をクリックします。
Step7
「コンテナ名」を入力し【作成】をクリックします。

7. 「Google Analytics」と「Google Optimize」の連結


Step1
コンテナのダッシュボードにアクセス出来ている事を確認し【設定】をクリックします。
Step2
【アナリティクスへリンクする】をクリックします。
Step3
作成したGoogle Analyticsのプロパティを選択します。

現状(2020/12/09時点)は、GA4のプロパティは表示されません。
プロパティとリンクさせないとOptimizeは使えないので、ユニバーサルアナリティクスプロパティでないと、Optimizeは使えないことになります。


8. 「Next.js」にGAとOptimizeのIDを書き込む

GAとOptimizeのリンクに成功するとIDが分かるので、これを_document.jsに書き込みます。

import React, { Fragment } from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const isProduction = process.env.NODE_ENV === 'production';

    const initialProps = await Document.getInitialProps(ctx);

    return { ...initialProps, isProduction };
  }

  render() {
    const { isProduction } = this.props;

    return (
      <Html>
        <Head>
          {isProduction && (
            <Fragment>
              <script async src="https://www.googletagmanager.com/gtag/js?id=UA-185122844-1" />
              <script
                dangerouslySetInnerHTML={{
                  __html: `
                  window.dataLayer = window.dataLayer || [];
                  function gtag(){dataLayer.push(arguments);}
                  gtag('js', new Date());
                  gtag('config', 'UA-185122844-1', { 'optimize_id': 'OPT-MZN2N7K'});
                `
                }}
              />
            </Fragment>
          )}
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument;

package.jsonの設定を変更する

後述のNetlifyでのbuildをするため、package.jsonのscriptを以下のように変更します。

"scripts": {
  "dev": "next",
  "build": "next build",
  "export": "next export",
  "deploy": "npm run build && npm run export",
  "start": "next start",
  "test": "echo \"No test specified\" && exit 0"
},

RepositoryにPushする

「Next.js」の設定は全て完了したので、

  • GitHub
  • Bitbucket
  • GitLab

いずれかにRepositoryを作り、Pushしておきます。


9. 「Netlify」の設定

事前にNetlifyのダッシュボードにログインしてください。

Step1
【New site from Git】をクリックします。
Step2
PushしたGitのサービスをクリックします。
Step3
PushしたRepositoryを選択します。
Step4
Build command → npm run deploy
Publish directory → out/
と入力し【Deploy site】をクリックします。
Step5
Deploy中のログをクリックします。
Step6
Deployが終了し「Site is live ✨」と表示されるか確認します。
Step7
【Site overview】をクリックし、URLが表示されていれば成功です。

10. 「Google Optimize」でのテスト

  1. 「エクスペリエンス」の作成
  2. デザインの変更
  3. 「パターンの比重」の変更
  4. 「測定と目標」の変更
  5. Optimizeの動作確認
  6. 「アクティベーションイベント」の設定
  7. テスト開始

の7つに分解して解説します。

1. 「エクスペリエンス」の作成

Step1
コンテナを選択して【開始】をクリックして、エクスペリエンスを作成します。
Step2
テスト名とテストするページのURLを入力し【作成】をクリックします。

2. デザインの変更

Step1
【パターンを追加】をクリックします。
Step2
「パターン名」を入力し【完了】をクリックします。
Step3
【編集】をクリックします。
Step4
ボタンを選択し【要素を編集】【HTMLを編集】をクリックします。
Step5
↑ Before ↑
↑ After ↑
こちらのソースコードに変更して【適用】をクリックします。

以下コピペ用です。
<span style="background: rgb(255, 0, 255); color: rgb(255, 255, 255); padding: 16px; margin-top: 16px; border-radius: 8px; cursor: pointer;">LPのボタンを変更しました</span>

Step6
デザインが変更されているのを確認し【保存】【完了】をクリックします。

3. 「パターンの比重」の変更

Optimizeでの動作テストを行いたいので、オリジナルのデザインを表示させないために、追加したパターンの表示比重を100%にします。

実際のA/Bテストでは、オリジナルと変更したデザイン半分ずつ表示させてテストを行うので、比重を変更する必要はございません。

Step1
【比重】をクリックします。
Step2
【カスタムの割合】に変更して、追加したパターンの比重を100%にして【完了】をクリックします。

4. 「測定と目標」の変更

Step1
「測定と目標」【テスト目標を追加】をクリックします。
Step2
【リストから選択】をクリックします。
Step3
メインとなる目標を選択します。

実際のテストでは、目標となる項目がない場合【カスタム目標を作成】から目標を新規作成します。

5. Optimizeの動作確認

Step1
「設定」【インストールを確認】をクリックします。
Step2
正しくインストールされていることを確認し【ウェブサイト エクスペリエンスに戻る】をクリックします。

アクティベーションイベント後にテストをする場合、アンチフリッカースニペットを設定する必要はございません。
注意書きは無視していただいて構いません。

6. 「アクティベーションイベント」の設定

Step1
「設定」「アクティベーションイベント」から【ページの読み込み】をクリックします。
Step2
【カスタムイベント】をクリックします。
Step3
イベント名がoptimize.activeになっていることを確認し【完了】をクリックします。

7. テスト開始



【開始】をクリックし「実行中」のステータスに切り替わったら、テストが開始されます。

テストが正常に反映されていたら完了です。

ご注意

テストの確認はGoogle Chromeのシークレットブラウザで行ってください。
キャッシュの影響でテストが正常に反映されない可能性があります。


初期設定のままテストを行った場合

結論からいうと、ページ読み込み時に一瞬テスト結果が反映され、その後JavaScriptがテスト結果を上書きします

また非同期通信はページ遷移時に読み込みが発生しないので、ページ遷移した際テスト結果が反映されません。

初期設定のままテストを行うと、テスト結果が反映されない。

その為テスト有効化のタイミングを、アクティベーションイベント発火後に設定するのです。


どのライブラリでもイベント発火後にテストを実施しないといけない



フロントエンドのライブラリを支える技術を「Single Page Application(SPA)」といいます。
非同期通信の技術を使って、JavaScriptがDOMを操作してページを切り替えています。

基本的にはどのライブラリでもA/Bテストを行う場合「アクティベーションイベント発火後」に設定する必要があります。

「非同期通信」という共通の技術を使っているからです。
JavaScriptがテスト結果を上書きしてしまうので、必ずイベント発火後にテストを実施しないといけません。


余談:Google Tag Manager(GTM)は公式非推奨

前述のアンチフリッカースニペットの項目にもありますが、Google Tag Managerを使ってOptimizeタグを配信することは、公式から非推奨の勧告が出ています。

GTMを使う場合、Optimizeコンテナを読み込む前にGTMコンテナを読み込みます。
両方読み込むまでテストが開始されないので、数値に誤差が出る可能性があります。

正確な測定結果が重要な「仮説検証」で、数値の誤差が出るのは致命的です。
その為OptimizeのタグはGTMを使わず、直接埋め込みOptimizeコンテナを先に読み込ませます。


最後に

SPAとOptimizeの設定を解説した記事はいくつかあります。
私も過去にGatsbyでOptimizeを使う方法を解説した記事を書きました。

ですが、環境構築からテストまで一気通貫で、かつ画像入りで解説した記事はなかったので、自分で執筆してみました。

SPAでOptimizeを使い始めた際は、アクティベーションイベントの件は全く知らなかったので、数値の誤差が出ているテストを、自分が気付かない内に執り行っていました。

テスト結果が反映されなくても、Optimizeではデザインを変更したBパターンとして記録されます。
なので気付きにくいのです。

今回は以上になります。

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

Redux ToolkitでLoadingの状態を管理する

何をやるか

SPAを扱う上で、どのタイミングでローディングの表示をするのか、どのように管理するのか。
結構考えるのめんどくさいと思います。
↓こんなやつ
b39f6ff59f51a613294a5f21ef9382de.gif

Redux Toolkitを使えばシンプルにかけたので備忘録として残しておきます:writing_hand:
どのようにローディング状態管理しよう…とお考えの方がいれば、その解決の一つになればと思います:eyes:

使用している技術

  • React (reactHooks)
  • Redux (Redux-toolkit)
  • TypeScript

機能

今回はAPIを叩いてpending中にloadingを表示、
処理が終了したらloadingを非表示という機能を想定して書いていきます。

redux toolkitが用意してくれているextraReducersという機能を使って実装します!

pending → 実行開始〜結果が帰ってくるまでの間
fulfilled → 正常終了
rejected → エラー

となっているので、以下のように、
pendingの時のみ、isLoadingという項目をtrueにすることで、
APIが実行開始〜結果が帰ってくるまでの間の状態を管理します。

sampleSlice.tsx
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { fetchGet } from "./common/fetch"

interface selectUserProps {
  isLogin: boolean
}

// ↓このAPIを叩く想定で進める
export const getInfo = createAsyncThunk("admin/post",async () => {
  const url = "叩くAPIのURL"
  const result = await fetchGet(url)
  return result
  }
)

const sampleSlice = createSlice({
  name: "sample",
  // ↓初期値
  initialState: {
    isLoading:false
  },

  // extraReducersで状態をLoading管理する
  extraReducers: (builder) => {
    builder.addCase(getInfo.pending, (state, action) => {
      // APIの処理が始まったら isLoadingをtrueに
      state.isLoading = true
    })
    builder.addCase(getInfo.rejected, (state, action) => {
      // 失敗したら isLoadingをfalseに 
      state.isLoading = false
    })
    builder.addCase(getInfo.fulfilled, (state, action) => {
      // 成功しても isLoadingをfalseに 
      state.isLoading = false
    })
  },
})


// ローディング状態をexport
export const selectLoading = (state: any): selectUserProps => state.sample.isLoading
export default sampleSlice.reducer

fetch.ts
// 別に切り分けた関数
import axios from "axios"
axios.defaults.withCredentials = true

export const getInfo = async(url:string) =>{
  const result = await axios(url, {
    method: 'GET',
    )
  return result.data
}

UI

UI上では、スピナーを作成して、
先程のloading状態をインポートし、trueの場合だけ作成したスピナーを表示させます。

スピナー

画面全体にLoadingを表示させるコンポーネントです。
(待っている間はユーザー側の入力ができないので、ユーザー操作できるようにするには調整必要です)

fixedSpinner.tsx
import React, { useEffect, useState } from 'react'
import { css } from '@emotion/core'
import { Spinner } from '../../components/atoms/Spinner'

export const FixedSpinner = () => {
  return (
    <div css={spinnerOverRay}>
      <div css={spinnerWrap}>
        <Spinner />
      </div>
    </div>
  )
}

const spinnerOverRay = css`
  position: fixed;
  -ms-transform: translate(-50%, -50%);
  height: 100vh;
  width: 100vw;
  z-index: 999;
  background-color: rgba(245, 245, 245, 0.5);
`
const spinnerWrap = css`
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
`
Spinner.tsx
import React from 'react'
import { css } from '@emotion/core'

export const Spinner = () => {
  return <div css={spinner} />
}


export const spinner = css`
  width: 50px;
  height: 50px;
  border: 10px solid #dddddd;
  border-top-color: #aaaaaa;
  border-radius: 50%;
  animation: 1s spin infinite linear;
  @keyframes spin {
    from {
      transform: rotateZ(0deg);
    }
    to {
      transform: rotateZ(360deg);
    }
  }
`

Loading表示

useSelectorを使って、loading状態を取得し、
trueの場合のみ、先程作成したスピナーを表示します。

sample.tsx
import React from 'react'

// components
import { FixedSpinner } from '../../components/block/FixedSpinner'

// store
import { getInfo, selectLoading } from '../../../stores/slices/sampleSlice'

export const Sample = ({ setPath }: any) => {
  const dispatch = useDispatch()
  const loading = useSelector(selectLoading)

  const handleSubmit = () => {
    dispatch(getInfo())
  }

  return (
    <div>
      // loading が true の時、FixedSpinnerを表示
      {loading && <FixedSpinner />}
       <button css={SubmitBtn} onClick={handleSubmit}>
         API叩く
       </button>
    </div>
  )
}

感想

Redux Toolkitを使えば簡単にloadingの管理ができてハッピーでした:clap:
コンポーネントも使いまわせるので、使える部分はこれ使っていこうかな〜と考えております。

もっといい方法あればご教示ください:pray:

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

Ant designでカスタマイズしたもの、使いやすくしたもの

Ant designとは

https://ant.design/
Reactコンポーネントを、すぐに使える形(デザインされたもの)で返してくれる。
同時に、基本的な機能を揃え、開発をスムーズにしてくれる。

カスタマイズしたもの

省略した文字+ツールチップ

// 省略文字(かつ長い時切り捨て)とツールチップの表示
export const EllipsisTooltip = (props) => {
  const { text, toolTipText, maxSize, children } = props;
  const flgTruncate = maxSize ? text.length > maxSize : false;
  return (
    <Space>
      {flgTruncate ? text.slice(0, maxSize) + '' : text}
      {(text !== toolTipText || flgTruncate) && <Tooltip title={toolTipText}>{children}</Tooltip>}
    </Space>
  );
};

// use
<Link href={url} target="blank">
  <EllipsisTooltip text="" toolTipText={url}>URL <ExportOutlined />
  </EllipsisTooltip>
</Link>

テーブルのpaddingを狭め、子要素を囲むようなスタイルに変更

export const TableList = styled(Table)`
  .ant-table-thead > tr > th,
  .ant-table-tbody > tr > td,
  .ant-table tfoot > tr > th,
  .ant-table tfoot > tr > td {
    padding: 10px 10px;
  }
  .ant-table-tbody > tr.ant-table-expanded-row {
    > td {
      padding: 0;
    }
  }
  .ant-table-column-sorters {
    padding: 0;
  }
  .ant-table-tbody > tr > td > .ant-table-wrapper:only-child .ant-table {
    margin: 0;
  }
`;

// 子スタイル
export const ChildTable = styled(Table)`
  background-color: #ccc;
  .ant-table {
    font-size: 90%;
  }
  .ant-spin-container {
    /* padding: 24px 22px 20px 22px; */
    padding: 10px;
    background-color: #ccc;
    position: relative;
    &:before {
      content: '';
      display: block;
      position: absolute;
      left: 72px;
      top: 5px;
      width: 0;
      height: 0;
      border-style: solid;
      border-width: 0 8px 14px 8px;
      border-color: transparent transparent #fafafa transparent;
    }
  }
  .ant-table-thead > tr > th,
  .ant-table-tbody > tr > td,
  .ant-table tfoot > tr > th,
  .ant-table tfoot > tr > td {
    padding: 8px 10px;
  }
  .ant-table-content {
    overflow: auto !important;
  }

テーブル > ドロップダウンフィルター

dataから存在するデータをドロップダウンで表示

export const getFilterOption = (datalist, dataIndex) => {
  if (!datalist) return false;
  let array = [],
    filters = [];
  datalist.forEach((value) => {
    if (array.indexOf(value[dataIndex]) === -1) {
      array.push(value[dataIndex]);
      filters.push({ text: value[dataIndex], value: value[dataIndex] });
    }
  });

  return {
    filters: filters,
    onFilter: (value, record) => record[dataIndex].indexOf(value) === 0,
  };
};

テーブル > 単語検索フィルター

編集中

export const getSearchOption = (dataIndex) => {
  let searchInput;
  let searchState = {
    searchText: '',
    columnIndex: '',
  };

  const handleSearch = (selectedKeys, confirm, dataIndex) => {
    confirm();
    return { searchText: selectedKeys[0], columnIndex: dataIndex };
  };

  const handleReset = (clearFilters) => {
    clearFilters();
    return { searchText: false, columnIndex: false };
  };

  return {
    // eslint-disable-next-line react/display-name
    filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
      <div style={{ padding: 8 }}>
        <Input
          ref={(node) => {
            searchInput = node;
          }}
          placeholder={`検索したいキワードを入力してください`}
          value={selectedKeys[0]}
          onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
          onPressEnter={() => {
            searchState = handleSearch(selectedKeys, confirm, dataIndex);
          }}
          style={{ width: 188, marginBottom: 8, display: 'block' }}
        />
        <Space>
          <Button
            type="primary"
            onClick={() => {
              searchState = handleSearch(selectedKeys, confirm, dataIndex);
            }}
            icon={<SearchOutlined />}
            size="small"
            style={{ width: 90 }}>
            検索
          </Button>
          <Button onClick={() => handleReset(clearFilters)} size="small" style={{ width: 90 }}>
            リセット
          </Button>
        </Space>
      </div>
    ),
    // eslint-disable-next-line react/display-name
    filterIcon: (filtered) => (
      <SearchOutlined
        style={
          filtered
            ? { color: '#ffffff', backgroundColor: '#000', padding: '8px', borderRadius: '50%' }
            : { padding: '8px' }
        }
      />
    ),
    onFilter: (value, record) =>
      record[dataIndex] ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()) : '',
    onFilterDropdownVisibleChange: (visible) => {
      if (visible) {
        setTimeout(() => searchInput.select(), 100);
      }
    },
  };
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GAS の各種イテレーターを for ... of で使える反復可能オブジェクトにする

2020年のビッグニュースの一つとして、GAS で新しい JavaScript エンジンである V8 ランタイム(以下 V8)がサポートされたことでしょう。

V8 によって ES2015 以降の構文が(試した限りでは ES2019 の構文まで)使えるようになったため、モダンな文法を使ってプログラムを読みやすくすることが容易にできるようになりました。従来だと Clasp でローカルにソースコードを持ってきてローカルでは TypeScript で……という手段で ES2015 以降の文法を書くことができましたが、GAS 自体で公式サポートされることで楽が出来るしカジュアルに書きたい人と共同開発しやすいしと嬉しいことばかりです。

そんな ES2015 以降の文法で目を引くのがいわゆる for...of 文でしょう。

for...of 文とは

for...of 文と呼ばれるものは、従来からある for 文を書きやすくしたものです。

従来の for 文だと

var numbers = [200, 380, 400, 100, 80];
var sum = 0;
for (var i = 0; i < numbers.length; i++) {
    sum += numbers[i];
}
console.log("sum is " + sum); // GAS V8 以前では Logger.log が一般的

と配列の添字を使って配列の各要素を取得していたものが、 for...of 文では配列の添字を使わない書き方ができます。

const numbers = [200, 380, 400, 100, 80];
let sum = 0;
for (const number of numbers) {
    sum += number;
}
console.log("sum is " + sum);

ES2015 では var の欠点を払拭している変数宣言構文 letconst が使えるようになったので、 var の代わりに合わせて使ってみます。sum だけは += で再代入が発生するので let で宣言していますが、他は再代入が発生しないので const を使っています。1

今回は繰り返す numbers は配列すなわち Array ですが、実はこの of の右側に置くことができるのは Array だけでなく反復可能オブジェクト (iterable object) を置くことができます。Arrayも反復可能オブジェクトの一つというわけですね。

反復可能オブジェクトはそのオブジェクトが意味的に要素の集まった何かと解釈できるものと言えそうです。このあたりは、JavaScript の準公式ドキュメントとも言える MDN Web Docs のページが詳しいので参照してみて下さい。

まさに要素の集まりを表すと言える MapSet の他、String も「一文字ずつ」という意味で反復可能オブジェクトとして振る舞うようです。

反復可能オブジェクトは英語 iterable object の和訳ですが、iterator はカタカナ語「イテレーター」としてもよく知られています2。そして GAS の一部のオブジェクトにはイテレーターを提供するものもありますが、for...ofof の右側に置くことはできないようです。

次の節では、GASで出てくるイテレーターを for...of で使えるようにする方法を見ていきたいと思います。

GAS の XxxIterator を反復可能オブジェクトにする

GAS は日々拡張を繰り返していますが、「Advanced Google service」を除いた基本的な「Google Workspace service」の中でイテレーター XxxIterator 的なものは Drive ServiceFileIteratorFolderIterator のみのようです(この記事を書く前はもっと多いと思っていました)。

実際の使用例を FileIterator を例に見てみます。

const FOLDER_ID = "自分が読み取り権限のあるフォルダのフォルダID";
function listdir() {
  const folder = DriveApp.getFolderById(FOLDER_ID);
  const files = folder.getFiles(); // FileIterator
  while (files.hasNext()) {
    const file = files.next();
    console.log(file.getName());
  }
}

FileIterator も FolderIterator も以下のような性質を持つオブジェクトです。

  • 次にあたる要素があれば真偽値 Boolian を返す hasNext メソッドの実行結果は true、そうでない場合は false を返す
  • hasNext メソッドの実行結果が true であれば、next メソッドを一回実行して次に当たる要素を表すオブジェクトを得る
    • FileIterator.prototype.nextFile オブジェクト、FolderIterator.prototype.nextFolder オブジェクトを返す
    • hasNext メソッドの実行結果が false のときに next メソッドを実行すると例外が発生する
  • なお、FileIterator も FolderIterator も、for...of のof の右側に置くと反復可能オブジェクトではないという例外が発生する

返す値が違うだけで、hasNextnext の挙動はほぼ同じと言えましょう。なので動的言語らしくダックイピング的に FileIterator と FolderIterator を区別することなく反復可能オブジェクトにしてみましょう。

配列に入れる

単純な発想ですが、事前に全ての結果を集めた配列を返すことで反復可能オブジェクトとする方法もあるでしょう。もっとエレガントな解法は後述しますが、対比のため配列に入れる作戦も見ていきましょう。

const FOLDER_ID = "自分が読み取り権限のあるフォルダのフォルダID";
function listdir() {
  const folder = DriveApp.getFolderById(FOLDER_ID);
  const files = folder.getFiles();
  for (const file of iterator2arrayGAS(files)) {
    console.log(file.getName());
  }
}

function iterator2arrayGAS(iteratorGAS) {
  const elements = [];
  while(iteratorGAS.hasNext()) {
    elements.push(iteratorGAS.next());
  }
  return elements;
}

最初にエレガントな解法を思いつければよいのですが、問題解決をしたいけれど使える時間に限りがある場合は、こういう既知の内容で解決することも立派な解決方法です。

この配列に入れる作戦の唯一心配なところは、hasNext を途方も無い回数呼んでも false とならず、結果の配列の要素が限りなくなる可能性を考えたときでしょうか。世間でよくある RDBMS の SELECT 結果を全件配列で受け取らずイテレーター的手法を使うのも、膨大な数を一気に取得する可能性を考慮するとメモリや時間的なコストが非現実的となる場合があるからでしょう。

筆者は GAS のフォルダ直下に置けるファイルの最大数を知りませんが、Google Drive のフォルダ直下に置けるファイル数の上限がよく知られていないこと、GASの「6分の壁」を考えると、配列に入れようが後述のエレガントな解答であろうが膨大なファイル数である可能性を排除できない場合は問題が発生する可能性があります。その点については別途考察しましょう。

反復処理プロトコルに沿って書いてみる

反復可能オブジェクトを自作するには、反復処理プロトコルに沿うと良いでしょう。

詳細は上記 MDN Web Docs の解説記事を見ていただくことにして、先ほどの listdir 関数を反復処理プロトコルに従って書き換えてみます。

const FOLDER_ID = "自分が読み取り権限のあるフォルダのフォルダID";
function listdir() {
  const folder = DriveApp.getFolderById(FOLDER_ID);
  const files = folder.getFiles();
  for (const file of iterableGAS(files)) {
    console.log(file.getName());
  }
}

function iterableGAS(iteratorGAS) {
  return {
    next: function() {
      const done = !iteratorGAS.hasNext();
      const value = done ? undefined : iteratorGAS.next();
      return {value: value, done: done};
    },
    [Symbol.iterator]: function() { return this; }
  };
}

上記 MDN Web Docs の解説記事を読んでなんとなく理解できるのであれば、上記書き換えコードの iterableGAS 関数のやろうとしていることもわかるかと思います。もしわからなくても大丈夫です、きっと興味を持って勉強を続けていればいつか必ずわかります。

ジェネレーターに沿って書いてみる

ES2015 以降には for...of などと合わせてジェネレーター関数というものも導入されました。

function 宣言ではなく function* 宣言。この function* 宣言で定義されたジェネレーター関数は、実行結果として定義内の return 結果を返すのではなく、定義に即した Generator オブジェクトを返します。この Generator オブジェクトは反復可能プロトコルに準拠したものとなります。

正確性より直感を重視して解説すると、通常の function 宣言で定義された関数は、一度の実行で一箇所の return 結果を返すものですが、function* 宣言で定義されたジェネレーター関数は、一度の実行で yield に出会うまでブロック内実行を進めそこの yield で指定された値を返し一時停止、次の値を要求されたらさらに次に yield に出会うまで……を return に出会うか関数定義の末尾に至る(結果的に暗黙で return undefined する)まで続ける、といったニュアンスになっています。全部の値を集めた配列を返すのと違い、逐次返却するのでメモリ的に優しいことと、終わりがないデータにも対応可能な点などの違いがあります。

説明ばかりしてもわかりづらいと思うので、listdir 関数をジェネレーターに沿って書き換えてみたいと思います。

const FOLDER_ID = "自分が読み取り権限のあるフォルダのフォルダID";
function listdir() {
  const folder = DriveApp.getFolderById(FOLDER_ID);
  const files = folder.getFiles();

  for (const file of generatorGAS(files)) {
    console.log(file.getName());
  }
}

function* generatorGAS(iteratorGAS) {
  while(iteratorGAS.hasNext()) {
    yield iteratorGAS.next();
  }
}

反復処理プロトコルを直接書いて return とオブジェクトが入り乱れていた iterableGAS と比べると、generatorGAS の定義は非常にシンプルになりました。むしろ最初に whilehasNextnext をで直接書いていたものと見かけほぼ同じ構造になりつつ、そのコードをまるごと別関数(ジェネレーター関数ですが)に分けることができました。

膨大な数の処理が必要なときの対処法

今回の for...of と反復可能オブジェクトの話題とは直接関係はありませんが、 FileIterator や FolderIterator で膨大な数の処理が必要で、実際に行いたい処理を入れても6分に収まらない場合はどうすればいいでしょう。

FileIterator のドキュメントを読むと getContinuationToken というメソッドが見つかります。このメソッドは String として継続トークンを返し、その継続トークン continuationTokenDriveApp.continuFileIterator に引数として渡すことで、getContinuationToken を取得した時点の FileIterator オブジェクトを返してくれるということです。

筆者の環境では、このような膨大な数……というサンプルが無く、そもそも処理対象ディレクトリに6分の壁を意識させるほどのファイル数があるフォルダも無いので課題解決意欲がわかないこともあり、このあたりは実験していません。とはいえ、そういう問題に行き当たったときに継続トークンが取れることを思い出せれば良いでしょう。

配列に入れるサンプル iterator2arrayGAS では、 最初に全件取ってしまうので getContinuationToken メソッドは無意味です、その後に課題となっている処理を行っている最中に時間切れ寸前……というときに files はもう全て取り尽くして next を呼べないイテレーターだからです。この意味でも、配列に入れるサンプルは分が悪いと言えます。

反復可能プロトコル iterableGAS やジェネレーター関数 generatorGAS のサンプルでは、getContinuationToken を呼ぶ意味があります。for...of ブロック中で時間切れ間近となった場合、ブロックを break で出た後で const token = files.getContinuationToken(); として String token の結果を適当なスプレッドシートのセルやスクリプトプロパティなどの永続的な場所に書いて次の実行時の起点とすることができるでしょう。もちろん、永続的な場所に書くだけの時間的余裕は持ちましょう。

そうはいっても配列が欲しいとき

膨大な数のことなどを想定して反復可能オブジェクトを返すようにした場合も、「今回の場合は膨大な数なんてありえないし Array.prototype.map で一気に処理したい」といった要望はよくあるでしょう。

そういうときは、反復可能オブジェクトを Array.from に引数として与えることで、反復可能オブジェクトを配列に変換することができます。

const FOLDER_ID = "自分が読み取り権限のあるフォルダのフォルダID";
function listdir() {
  const folder = DriveApp.getFolderById(FOLDER_ID);
  const files = folder.getFiles();
  const itr   = generatorGAS(files); // 反復可能オブジェクト、iteratorGAS でも同じ意味の反復可能オブジェクトを得られる
  const all_files_text = Array.from(itr).map( file => file.getName() + "\n" ).join("");
  console.log(all_files_text);
}

function* generatorGAS(iteratorGAS) {
  while(iteratorGAS.hasNext()) {
    yield iteratorGAS.next();
  }
}

GAS の Advanced Google service の list メソッドを反復可能オブジェクトにする

GAS には Advanced Google service というものもあり、有効化までひと手間必要ですが、最初から有効な Google Workspace service では使えない Google サービスへのアクセス API を使うこともできます。

ただ、Advanced Google service は RESTful API のアクセス手順をそのまま持ってきた API デザインとなっており、Google Workspace service の XxxApp を起点にしたあの感触とは結構違ったものとなっています。

具体的には、今回のイテレーターのような全件返さないものは、イテレーターというよりページャのような返却方法を取っています。

例えば Google Tasks を操作する Tasks Service を見ると、 Tasks.Tasklists.listTasks.Tasks.list といった list メソッドが各ページを返す役割となります。筆者は Google Tasks で自分にフィットしたタスク管理をしようと Google Tasks API を操作した経験を元に書いていますが、他の Advanced Google service でも list メソッドが各ページを返すという構造となっているようです。

Tasks API のリファレンスドキュメント から Tasks.Tasks.list の解説 を見ると、以下のようになっていることがわかります。

  • タスクリストのタスクリストID tasklistId を第一引数に指定した const tasks = Tasks.Tasks.list(tasklistId) という呼び出して、指定タスクリストに所属するタスク群の1ページ目のオブジェクト tasks が手に入る
    • 第2引数に検索オプションを指定できる、詳細はリファレンスドキュメント参照
  • 上記で取得した1ページ目のオブジェクトのキーは kind etag items そして nextPageToken
    • items は1ページ目のタスク群
    • nextPageToken は次のページを指定するトークンで、次のページが存在しない場合には存在しない
  • 次のページを取得する場合は nextPageToken を確保しておいて、第2引数の検索オプション内で pageToken プロパティとして const tasks = Tasks.Tasks.list(tasklistId, {pageToken: nextPageToken}); とするとよい

反復可能プロトコルで書くよりジェネレーター関数の方が楽でもあるので、今回は最初からジェネレーター関数で書いてみましょう。

あと、RESTful API に最小限のメソッドしか無いので後々の拡張を想定して、これも ES2015 で導入された class 構文を使って書いてみましょう。

class PagesIterator {
  /**
   * constructor
   * 1st argument is callback. It is given only 1 argument as page token.
   * It must return object that have items property (required as Array) and nextPageToken property (optional, if next page is exist).
   * @param {function} token => token ? NEXT_PAGE_RETRIEVE_CODE : FIRST_PAGE_RETRIEVE_CODE
   * @return {PagesIterator}
   */
  constructor(callback) {
    this.callback = callback;
    const res = this.callback();
    this.current_response = res;
    this.safe_counter = 0;
  }
  /**
   * Retrieve next page if it is exist
   * This method is almost internal for @@iterator method.
   * @return {bool} retrieve success or fail
   */
  retrieve() {
    if ( !this.current_response.nextPageToken ) {
      return false;
    }
    const res = this.callback(this.current_response.nextPageToken);
    this.current_response = res;
    return res.items.length > 0;
  }
  /**
   * Iterator for this class and instance
   * This class'es instance can puts iterable context, e.g. for ( const item of THIS ) { ... }
   */
  * [Symbol.iterator]() {
    while(true) {
      this.safe_counter++;
      for ( const obj of (this.current_response.items||[]) ) {
        yield obj;
      }
      if ( !this.retrieve() ) break;
      if ( this.safe_counter > PagesIterator.SAFE_LIMIT ) {
        console.error("safe counter over: break");
        break;
      }
    }
  }
}
PagesIterator.SAFE_LIMIT = 20;

* [Symbol.iterator]() { ... } 部分が反復可能プロトコルでジェネレーター関数を指定している部分。class 構文中でメソッド定義する場合には function キーワードが無くせるので、それに伴って function* キーワードも単独の * になるところは注意点です。

1要素ずつ反復していくのですが、取得したページを全件取り出したときに nextPageToken があれば次のページを取り出すと言ったまとまった処理をする必要があり、この処理を retrieve というメソッドで抜き出せるだけでもジェネレーター関数内の定義が少なくなってスッキリします。

ジェネレーター関数の定義もそこそこ多いように見えますが、while(true){...} 無限ループを使っていることによる不安解消のため、ループ最大数を PagesIterator.SAFE_LIMIT 回に制限する安全が半分程度を占めています。

この PagesIterator クラスの使い方は constructor のコメントにも書かれている通り

const TASKLIST_ID = "タスク群を取り出したいタスクリストのID";
const pages = new PagesIterator(
    token => Tasks.Tasks.list(TASKLIST_ID, token ? {...optionalArgs, pageToken: token} : optionalArgs
);

と言ったように、コンストラクタ new PagesIterator の第1引数にコールバック関数を指定、そのコールバック関数は nextPageToken がある場合に第1引数としてそれを取り、ない場合は1ページ目のオブジェクトを返すようにします。

PagesIterator では、コンストラクタがコールバック関数を受け取るようにして hasNext next のときのような単純なダックタイピングができないことを吸収していますが、コールバック関数の返り値のオブジェクトが items プロパティとして配列を持っていることを想定しているので、半分程度は内部構造に依存したコードになっています。であれば list メソッドとその第1引数、第2引数の構造も PagesIterator に任せてもいいのではと、書いた後で考えたりもしましたが、どこまでやるかは色々な方針があるでしょう。retrieve メソッドへの分離ができなくてもいいのであれば、class 構文に頼ること無く generatorGAS 関数のような単一の関数に処理を収めることもできるでしょう。

PagesIterator のまとまったコードは xtetsuji/gas-xtutils に置いてありますので、興味があったら参考にしてみて下さい。


  1. fornumber もブロックスコープごとに const 宣言されるのでブロック中で再代入しなければ問題ありません。また sum だけ let なのが気持ち悪いということでしたら、for すら使わず Array.prototype.reduce を使って const sum = numbers.reduce((sum, number) => sum + number); と言った書法がおなじみです。 

  2. 動詞 「反復する」iterate の名詞形 iterator を計算機用語として和訳すると「反復子(はんぷくし)」といった言葉になると思うのですが、筆者が触れた多くの場面で「iterator」「イテレーター」と英語やカタカナ語のままで扱われている印象があります。 

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

Reactで神経衰弱を作ってみた

卒業できるかな?

See the Pen RwGKbRJ by kotani (@kotani) on CodePen.

はじめに

最近注目されているReactについて記事を書こうと思います。
Reactで検索してみると、既に入門的な情報が転がっていますが、それらを読んだだけでは
なかなかイメージがつきにくいこともあり、Reactを使って自分で作ってみることにしました。
Reactの公式ページには三目並べのゲームがチュートリアルで紹介されてましたが、
私は神経衰弱を作ってみることで、Reactを体験してみようと思います。

基本的な知識

Reactとは「ユーザインターフェース構築のための JavaScript ライブラリ」だそうです。

コンポーネントと呼ばれる小さい部品を組み合わせることでアプリケーションを構築します。
その1つ1つのコンポーネントは状態(State)を持っており、外部から渡された情報 props
に従ってその状態を変化させることで、そのコンポーネントの挙動の制御を行います。

コンポーネント同士を疎結合にすることで、再利用したり管理しやすくなり、
どちらかというと大規模アプリケーション開発に向いていると言われてます。
なので、アニメーションといった実装には向いてないと思います。

そのほかにも、仮想DOMという仕組みによって高速描画を行ったり、
JSXの記法でJavaScriptの中にHTML似の構文を使える技術も取り入れています。

シングルページアプリケーション(SPA)開発にも適しており、iPhoneアプリや
androidアプリのように、ReactNativeフレームワークを使うことで、どちらの環境でも
動くアプリ開発もできるようです。

神経衰弱の概要

ゲームのイメージは、下記の通りです。

  • 5種類の絵柄が書かれた2対のカード合計10枚が伏せてあります。
  • スタートボタンをクリックするとカウントダウンタイマーが作動しゲーム開始となります。
  • 任意のカード1枚目をクリックすると、カードが反転し絵柄が表示されます。
  • 続けて別の2枚目のカードをクリックすると、同様に反転し絵柄が表示されます。
  • 2枚のカードの絵柄が揃ったら、カードの色をピンク色に変更してそのままにします。
  • 2枚のカードの絵柄が揃わなかったら、1秒程度後に2枚とも元の状態へと伏せます。
  • 全てのカードの絵柄が揃った場合は「おめでとう!」のメッセージが表示されます。
  • 全てのカードの絵柄を揃えることができないままタイムアウトになってしまった場合は
    「ゲームオーバー」のメッセージを表示させます。
  • カードが揃ったかどうかの判定結果は、下の方に「あたり」「はずれ」のメッセージを表示させます。

このような内容のゲームをReactで実装してみます。

準備

Reactを使った本格的な開発は、create-react-app という環境構築ツールを使うようですが、
ここでは、下記jsを外部から取り込むだけで使えるようになるお手軽な方法で実装します。

<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

HTMLの body 終了タグの直前に上記3行を記述して、scriptタグにtype="text/babel" 属性を追加します。JSX構文はまだ今のブラウザには対応していないので、Babelというツールを使って後方互換バージョンへ変換することで使えるようになっています。また、カードの画像を1個用意して、10枚分のカードが並んだ状態にCSS調整しておきます。

コンポーネントの説明

ここでは、コンポーネントの少し具体的な内容をコードと共に紹介していきます。

1 構成

下記のように、3つのコンポーネントで構成されています。

①Gameコンポーネント(②の親コンポーネント)
      |
②Tableコンポーネント(③の親コンポーネント)
      |
③Cardコンポーネント

2 Gameコンポーネント

下記は、divタグのところにReactコンポーネントを組み込む処理の記述になります。

<div id="root"></div>

React.Component クラスを継承したGameクラスを定義し、render()メソッドの中でGameコンポーネントを記述します。Gameコンポーネントの中にTableコンポーネントが含まれていることが分かるかと思います。

class Game extends React.Component {
    render() {
        return (
            <div className="game">
                <div className="main">
                    <Table />
                </div>
            </div>
        );
    }
}
ReactDOM.render(
    <Game />,
    document.getElementById('root')
);

3 Tableコンポーネント1(初期化)

コードが長いので分割します。TableコンポーネントもReact.Componentを継承します。
constructorの中では、getInitialState()をコールして初期化した各種状態のプロパティを持つオブジェクトをstateに格納します。shuffleは、毎回絵柄の並びが入れ替わるように配列内部をランダムに置換します。

class Table extends React.Component {
    constructor(props) {
        super(props);
        this.state = this.getInitialState();
    }
    getInitialState = () => {
        let arr = Array("", "", "", "", "", "", "", "", "", "");
        let sts = Array(10).fill(0);
        arr = this.shuffle(arr);
        return {
            cards: arr,
            status: sts,
            ready: -1,
            message: '',
            count: 15,
            timer: null,
            title:'',
            run:false,
            overlay: 'overlay'
        }
    }
    shuffle = ([...array]) => {
        for (let i = array.length - 1; i >= 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
        return array;
    }

    :
    :

}

4 Tableコンポーネント2(click時処理)

handleClickは、カードをクリックした時に呼ばれるメソッドです。
i はカードのindexであり、クリックされたカードに紐づいた数値(0~9)が渡されます。
stateで持っている状態は下記の通りです。

  • cards :絵柄 ("♡"や"♧"など)の配列
  • status : カードの状態を表す配列 0(初期値)、1(クリックされ絵柄が表示されている状態) 2(2枚の絵柄が揃った状態)、3(2枚の絵柄が異なった状態)
  • ready : クリックされたカードの状態 -1(初期値)、0~9(クリック1枚目のindex)
    -2(「はずれ」が表示された状態)
  • message : 判定後のメッセージ "" または "あたり" または "はずれ"
  • count : カウントダウンの数値(1づつ減算、残り:xx秒 のところに表示される)
  • timer : setInterval()の戻り値を格納
  • title : 画面中央に表示する文言 "" または "おめでとうございます。卒業です!" または "ゲームオーバー"
  • run : ゲームの状態 true(ゲーム実行中)、false(ゲーム停止中)
  • overlay : オーバーレイのクラス指定 "" または "overlay" または "overlay overlay-end"

readyの値を見て、1枚目がクリックされたのか?2枚目がクリックされたのか?の判定に使用しています。

handleClick(i) {
    const sts = this.state.status.slice();
    //未選択以外をクリックされた時は無視
    if (this.state.status[i] != 0) {
        return;
    }
    //ゲームスタートしてなければ無視
    let run = this.state.run;
    if (!run){
        return;
    }
    let ready = -1;
    let message = "";
    let title = "";
    let overlay = "";
    if (this.state.ready == -2) {
        return;
    } else if (this.state.ready == -1) {
        //1枚目をクリックした時の処理
        sts[i] = 1;
        ready = i;
    } else if (this.state.ready != i) {
        //2枚目をクリックした時の処理
        sts[i] = 1;
        //2枚揃ったかどうかを判定
        if (this.state.cards[this.state.ready] == this.state.cards[i]) {
            //2枚揃った!
            message = "あたり";
            sts[this.state.ready] = 2;
            sts[i] = 2;
            if (!this.isFinish(sts)) {
                setTimeout(() => {
                    this.cardClear();
                }, 800);
            } else {
                message = '';
                run = false;
                title = "おめでとうございます。卒業です!";
                overlay = 'overlay overlay-end';
                clearInterval(this.state.timer);
            }
        } else {
            //揃ってなかった!
            message = "はずれ";
            ready = -2;
            sts[this.state.ready] = 3;
            sts[i] = 3;
            //少し経過後に元に戻す
            const rollbacksts = this.state.status.slice();
            rollbacksts[this.state.ready] = 0;
            rollbacksts[i] = 0;
            setTimeout(() => {
                this.cardClear();
                this.cardReset(rollbacksts);
            }, 800);
        }
    }
    this.setState({
        status: sts,
        ready: ready,
        message: message,
        run: run,
        title: title,
        overlay: overlay
    });
}

5 Tableコンポーネント3(ゲーム開始)

gameStartは、スタートボタンが押された場合に呼ばれます。
runを参照して、既にゲーム実行中であれば何もせずにリターンして、
そうでない場合はsetInterval()を使ってカウントダウンタイマーを起動します。

gameStart= () => {
    if(this.state.run){
        return;
    }
    this.setState(this.getInitialState());
    const timer = setInterval(() => this.countDown(), 1000);
    this.setState({
        timer: timer,
        run: true,
        overlay: ''
    });
}

6 Tableコンポーネント4(その他)

カードの状態を元の伏せた状態に戻します。

cardReset = (sts) => {
    this.setState({
        status: sts,
        ready: -1
    });
}

わずかな時間「あたり」「はずれ」のメッセージを表示させたあとは表示削除します。

cardClear = () => {
    this.setState({
        message: ""
    });
}

全てのカードが揃ったかどうかを判定します。stsの値で2以外のものが1つでも存在したら、
まだ伏せてあるカードがあると判定します。

isFinish = (sts) => {
    let flg = true;
    for (let i = 0; i < sts.length; i++) {
        if (sts[i] != 2) {
            flg = false;
            break;
        }
    }
    return flg;
}

タイマーによって1秒ごとに呼ばれるメソッドです。
countの値を1づつ減らしていき、0になったらゲームオーバーと判定します。

countDown = () => {
    let nextCount = this.state.count-1;
    if(nextCount < 1) {
        this.setState({
            message: '',
            count: 0,
            run: false,
            title: 'ゲームオーバー',
            overlay: 'overlay overlay-end'
        });
        clearInterval(this.state.timer);
    } else {
        this.setState({
            count: nextCount
        });
    }
}

カードの並びを構成するHTMLをJSXで定義します。

renderCard(i) {
    return (
        <Card key={i}
            number={this.state.cards[i]}
            ready={this.state.status[i]}
            onClick={() => this.handleClick(i)}
        />
    );
}
render() {
    const cards = [];
    for (let i = 0; i < 10; i++) {
        cards.push(this.renderCard(i));
    }
    return (
        <div>
            <button className="start-button" onClick={this.gameStart}>スタート</button>
            <div className="count-number">残り:{this.state.count}</div>
            <div className="table">
                {cards}
            </div>
            <div className="status">{this.state.message}</div>
            <div className={this.state.overlay}><p className="title">{this.state.title}</p></div>
        </div>
    );
}

7 Cardコンポーネント

関数型コンポーネントとして定義しています。
propsで渡されたreadyの値に従ってカードのclassName属性を設定します。(「裏」「表」「当たり」)

function Card(props) {
    let cardStyle = 'card card-ura';
    let numStyle = 'omote';
    switch(props.ready){
        case 1:
            numStyle = 'ura';
            break;
        case 2:
            numStyle = "ura atari";
            break;
        case 3:
            numStyle = "ura hazure";
            break;
        default:
            cardStyle = 'card card-omote';
            break;
    }
    return (
        <button className={cardStyle} onClick={props.onClick}>
            <div className={numStyle}><span>{props.number}</span></div>
        </button>
    );
}

最後に

Reactは、少々学習コストが高く感じられました。経験の浅い方が、これにいきなり入るのは
ちょっとハードルが高いように思います。上手に設計し構築することが出来るようになれば、
大規模なアプリでも耐えうる実力が十分ありそうなライブラリだということがわかりました。

コンポーネント間のデータの受け渡しはpropsを通じて行われますが、コンポーネントの
数が増えたり親子関係が深くなると、親から子へ通知する処理やコールバック関数が増えてしまい、
複雑化するようです。

そこで、React-Reduxという状態(state)管理ライブラリをうまく活用することで、
コンポーネント間の情報の受け渡しや状態管理をより簡単に実現することができます。

この神経衰弱は少しstate管理が煩雑になっているようなので、React-Reduxで実装した場合、
どのような書き方に変わるのか?どのような効果があるのか?を次回試してみようと思います。


:christmas_tree: FORK Advent Calendar 2020
:arrow_left: 17日目 Reactアプリケーション開発入門 @sy12345

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

【個人アプリ作成】情報管理アプリ制作14日目

また、時間が空いてしまいました。。継続性の難しさを痛感します。
今日までやってきたことをまとめたいと思います。

実装できてきたこと

・マイページのレイアウト修正
・顧客データの要件定義
・顧客データ詳細画面
・ログイン画面の修正
・情報の取得
・プルダウンの実装

マイページ

スクリーンショット 2020-12-15 11.45.03.png

少しヘッダーを整理してレイアウトをすっきりさせました。
今後はヘッダーのユーザーのところにJSで装飾を施したり、検索機能の実装を行う予定です。

今のところ、顧客情報は社名だけといった感じですが、かなり寂しいので「何の情報を開示できると有益なのか」をもう少し要件定義した方が良さそうです。
実装はruby on railsで、Seeds.rbにデータを入れて取得させています。

顧客データ詳細画面

スクリーンショット 2020-12-15 11.42.25.png

基本情報の部分は一通り必要な情報を印字させて、先方関係者やグループ会社、トラブル・クレームは履歴データで持たせたいので、テーブルを分けアソシエーションで対応したいと思います。

まだ、顧客データの登録機能や、情報編集機能には着手できていないのでこれから実装していきたいと思います。

これまでやってきた感想

かなり進捗が悪く、原因を追求してみました。

・本業の仕事がかなり忙しい(いい意味で)。
→夜遅くなるのが日常茶飯事なので時間調整をして20時や21時にはこちらに着手できるようにする
・ある意味、色々やりすぎていた。特に実績もないのにPFを頑張って作ろうとしていたのはまずかった。今はアプリ開発とQiita、Githubに力を入れる。協業は随時進める。
・プライベートの慌ただしさ。
こればっかりは結婚だの同棲だの異動だので不可避なのでありきで考える。これまで考えられていなかった。

想定される工数の見積もりが甘いのとあらためて仕事は影響の出ない範囲で調整をかけていくようにしたい。

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

react基礎について

reactはUI構築のためのライブラリである。
そもそもライブラリ・フレームワーク・UIとは何か?

ライブラリ

一言で言うと「様々なプログラムを保存しているところ」
プログラムを組んでいく中で何度も目にするコードがある。
それらを一部修正をして汎用性の高いコードや関数をまとめておく。
けど、便利なコードや関数をまとめてるだけで、ライブラリだけ動かすことは出来ない

メリット
・プログラムの動作が軽くなる
・プログラムの開発スピードが上がる
・開発者の勉強になる

フレームワーク

端的にアプリやシステム開発のために必要な機能を用意した枠組み
経験が浅くてもプログラムを作成することが出来る。

メリット
・開発コストを抑える。
・バグを減らす
・コードがシンプルになる

フレームワークは、必要なものをまとめた枠組のこと
ライブラリは、便利なソースや関数の部品の集まり

https://xtech.nikkei.com/it/article/lecture/20070205/260697/

ユーザーインターフェース

窓口みたいなもの
何か2つのものの間で情報などのやりとりを行うときの方法や方式

https://techacademy.jp/magazine/8364

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

Enterキーが押されたらSubmit

Enterキーが押されたらSubmitしたい

AngularでEnterキーが押されたらSubmitする関数を作ります。

なんかのような内容を見つけました。

<script>
  function enter(){
    if( window.event.keyCode == 13 ){
      document.form1.submit();
    }
  }
</script>

そしてinputタグからこの関数を呼べば、Enterを押した瞬間Submitになる!!
……とのこと。

<form name="form1">
  <input type="text" onkeypress="enter();">
</form>

最近はonkeypressではなくonkeydownが主流みたい。

GlobalEventHandlers.onkeydown

【参考】
[JavaScript] Enterキーで直接Submitする方法と無効化する方法

Angularだと以下のものが該当するみたいです。

イベントバインディング
@Input() と @Output() プロパティ
ユーザー入力

つまり<input>内に(keyup.enter)="update(box.value)と書き込めばいいのか……?

うん、つまりそういうことらしい。

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

node.jsを自分のMacにインストールした

備忘録として。
node.jsを自宅のMacにインストールしようと思う

Homebrewから今回はやってみた
HomebrewとはMacOSで広く使われるパッケージ管理システム。。らしい

Homebrewを下記URLからまずインストール
https://brew.sh/index_ja

ターミナルで
brew -v
とコマンドを入力したら下記のようにバージョンが表記されたらインストール完了

Homebrew 2.6.1
Homebrew/homebrew-core (git revision dfe59; last commit 2020-12-14)

そしたらいよいよnode.jsのインストールへ。

brew install nodebrew

インストールが完了したら下記コマンドで確認

nodebrew -v

nodebrew 1.0.1~~
上記のようなばーじょんから始まる長ったらしい文が表示されたらOK

続いて下記コマンドを入力

nodebrew ls-remote

したら、ズラーっとvX.XX.Xのような表記でバージョンが並べられる
どのバージョンでもいいが推奨のバージョンをインストールする方がいいと思われるので、
今回は推奨バージョンをインストールすることに。
調べたら下記コマンドで推奨バージョンをインストールできるらしい

nodebrew install-binary stable

僕の場合ここでエラーが出てしまった
調べたところフォルダを作ってあげなきゃいけないみたいで
こちらのコマンドで作成。。。

mkdir -p ~/.nodebrew/src

インストール完了したらバージョンを確認

nodebrew ls

下記のように表示されればOK
現在インストールされてるバージョンが羅列される
ちなみにcurrentの部分には現在使用中のバージョンが入る

v14.15.1

current: none

useの後に使用するバージョンを選択

nodebrew use v14.15.1

もう一度nodebrew lsを試したら

v14.15.1

current: v14.15.1

上記のようにcurrentに入る
次は環境パスの設定
環境パスとはフルパスを指定せずソフト名のみを指定するだけで
プログラムを起動できるようにシステムに予め設定しているパス。
'>>'の後には各SHELLごとに合わせたファイルを指定
自分はzshだったので~/.zprofileを指定

echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile

そしたらnodeコマンドを実行

node -v

インストールしたバージョンが表示されたらOK
以上でnode.jsインストール完了

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

Vue.jsでパンくずリスト(手動でページ設定)

はじめに

Vue.jsでパンくずリストを実装すべく調べながら進めたので、メモ書きとして残しておきます。
自動でリストを作成する内容ではありませんので、よろしくお願いします。

仕様について

TOPページから一覧ページを挟んでとあるページに遷移します。

実装

呼び出し側の中身
Example.vue
<div>
    <BreadCrumb :breadcrumbs="breadcrumbs"/>
</div>

<script>

import BreadCrumb from "/components/BreadCrumb";

export default {
  components: {
    BreadCrumb
  },
  data () {
    return {
      breadcrumbs: [
       {
          name: 'TOP',
          path: '/'
        },
        {
          name: 'Category', // 中間ページ
          path: '/category',
        },
        {
          name: 'ExamplePage' // とあるページ
        }
      ]
    }
};
<script>
パンくずリストのコンポーネント中身
BreadCrumb.vue
<div class="breadcrumb-area">
    <div class="breadcrumb-item">
      <div v-for="v in breadcrumbs">
        <div v-if="v.path">
          <router-link :to="v.path">
            <span class="link">{{v.name}}</span>
          </router-link>
          <span class="space">></span>
        </div>
        <div v-else>
          {{v.name}}
        </div>
      </div>
    </div>
  </div>
解説

例として最後のとあるページで記載しています。
子コンポーネントに渡すデータにそのページまでのnameとpathをdataに配列型に持たせることで、パンくずが増えていくイメージです。
とあるページでは、nameは保持させますが、pathを保持させないことで、v-ifを使ってリンクかただの文字を表示させるかを切り分けています。

↓こんな感じ
スクリーンショット 2020-12-15 10.16.22.png

最後に

手動でページ設定をする方法でパンくずリストを作成してみました。
ちなみに、自動でページ毎に設定をさせる方法もあるみたいですので、大量のページに使う方などはそちらを参考にして頂いた方がいいかもしれません。

https://hirakublog.com/code/218/
参考にさせていただきました。ありがとうございます。

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

apiの復習

今回のapiの部部に関わっているのはここ

let xhr = new XMLHttpRequest(); //1

    xhr.open('GET', requestUrl);//2

    xhr.send();

    xhr.onreadystatechange = function () {//3
        if (xhr.readyState == 4) {
            ShowTodayWeather(xhr.responseText);
        }
    }

    function ShowTodayWeather(response) {
        let obj = JSON.parse(response) //4
        let weather = obj.weather[0].description; //5
        let city = obj.name;
        let temp = obj.main.temp;
        let humi = obj.main.humidity;
        let icon = obj.weather[0].icon;

        console.log("現在" + city + "の天気は" + weather);
        console.log(`気温は ${temp} 度です`);
        console.log(icon)

        const Tem = document.getElementById("tem");
        const Hum = document.getElementById("hum");

        Tem.innerHTML = temp
        Hum.innerHTML = humi
        Icon.setAttribute('src','http://openweathermap.org/img/w/'+icon+'.png')   
    }

mdnではこちら

1つずつ見ていく

1ではXMLHttpRequestオブジェクトを作成
newに関しては別でも勉強してまとめた。

2ではGETメソッドを使ってローカルサーバ上のurlを指定している。
情報を取得する時にはGET
情報を送信する時にはPOSTメソッドを使う。

3では1での処理の状況を監視している。
ここのreadyStateが4なのは4がリクエスト処理の完了を表すからである。

4のJSON.parseはjsonデータで受け取った天気情報をjavascriptで扱えるようにするメソッドである。

もしJavascriptからjsonデータにしたいときにはJSON.stringifyメソッドを使う。

これらをデコード・エンコードと言う。

5では日本語で取得するために最後descriptionとつけている。

それ以下は公式ドキュメントを参考にデータを取得した。

https://reffect.co.jp/html/xmlhttprequest-basic

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

OpenWeatherMapのapiの復習

今回のapiの部部に関わっているのはここ

let xhr = new XMLHttpRequest(); //1

    xhr.open('GET', requestUrl);//2

    xhr.send();

    xhr.onreadystatechange = function () {//3
        if (xhr.readyState == 4) {
            ShowTodayWeather(xhr.responseText);//4
        }
    }

    function ShowTodayWeather(response) {
        let obj = JSON.parse(response) //5
        let weather = obj.weather[0].description; //6
        let city = obj.name;
        let temp = obj.main.temp;
        let humi = obj.main.humidity;
        let icon = obj.weather[0].icon;

        console.log("現在" + city + "の天気は" + weather);
        console.log(`気温は ${temp} 度です`);
        console.log(icon)

        const Tem = document.getElementById("tem");
        const Hum = document.getElementById("hum");

        Tem.innerHTML = temp
        Hum.innerHTML = humi
        Icon.setAttribute('src','http://openweathermap.org/img/w/'+icon+'.png')   
    }

mdnではこちら

1つずつ見ていく

1ではXMLHttpRequestオブジェクトを作成
newに関しては別でも勉強してまとめた。

2ではGETメソッドを使ってローカルサーバ上のurlを指定している。
情報を取得する時にはGET
情報を送信する時にはPOSTメソッドを使う。

3では1での処理の状況を監視している。
ここのreadyStateが4なのは4がリクエスト処理の完了を表すからである。

4のresponseTextはxhrのサーバーから受け取ったテキストを返している。

mdn
https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest/responseText

5のJSON.parseはjsonデータで受け取った天気情報をjavascriptで扱えるようにするメソッドである。

もしJavascriptからjsonデータにしたいときにはJSON.stringifyメソッドを使う。

これらをデコード・エンコードと言う。

6では日本語で取得するために最後descriptionとつけている。

それ以下は公式ドキュメントを参考にデータを取得した。

参考サイト
https://www.sejuku.net/blog/47716
https://reffect.co.jp/html/xmlhttprequest-basic

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

【JavaScript】 料金の自動計算を非同期でActiveHash(アクティブハッシュ )のプルダウン選択をして表示させる。

はじめに

javascriptとプルダウンの選択方式で(アクティブハッシュ)非同期的に表示を変える方法を紹介します。
具体的には、予め決められた「料金」に対して、アクティブハッシュ内からdate属性で「数列」を取得し、計算をする実装をします。
要するに、「料金」X「人数」です。

ezgif.com-gif-maker.gif

前提
基本的なMVCとアクティブハッシュは既に実装済みです。
こちらを参考にしてください。

【Rails】ActiveHash(アクティブハッシュ)の導入と「確認」→「保存」→「表示」
https://qiita.com/AKI3/items/022ad68e22f798d40ad9

目次

  1. date属性の付与
  2. javascrip

開発環境

ruby 2.6.5
rails 6.0.0

実装

それでは実装していきます〜

1. date属性の付与

マイグレーションファイルの編集

db/migrate/2020XXXXXXXX_create_event.rb
class CreateEvents < ActiveRecord::Migration[6.0]
  def change
    create_table :events do |t|
      t.string    :price
      t.integer   :num_id

      t.timestamps
    end
  end
end

保存で必要なカラムpricenum_id(ActiveHashのid)を記述します。

rails db:migrate

モデル作成・編集

そして、ActiveHashモデルを作成します。
作成したActiveHashモデルの記述を編集します。

app/models/num.rb
class Num < ActiveHash::Base
  self.data = [
    { id: 0, name: '--' },
    { id: 1, name: '1', multiple:1 },
    { id: 2, name: '2', multiple:2 },
    { id: 3, name: '3', multiple:3 },
    { id: 4, name: '4', multiple:4 },
    { id: 5, name: '5', multiple:5 },
    { id: 6, name: '6', multiple:6 },
    { id: 7, name: '7', multiple:7 },
    { id: 8, name: '8', multiple:8 },
    { id: 9, name: '9', multiple:9 },
    { id: 10, name: '10', multiple:10 },
    { id: 11, name: 'お問い合わせください' },
  ]
end

name属性の後にdeta属性multiple:1と数列で数字を入れて追記します。
今回はmultipleをdeta属性を使用してますが、お好みで編集できます。
またmultiple:1の数列「1」も変更可能です。
例えば、こんな感じです。

id: 1, name: '1', multiple:1 
 #下記に様に編集できます
id: 1, name: 'コーラ', multiple:150 

とする事もできます。通常のActiveHashではname属性しか扱いませんが、date属性を使う事で様々な値を取得できます。
このdate属性は、データを取得際JavaScriptで取得します。

次にビューで入力フォームを作ります。

ビューの作成

app/events/viwes/index.erb
<h1>Events#new</h1>
<p>Find me in app/views/events/new.html.erb</p>

<div class="#">
  <div class="#">
    <P>一人あたりの値段</P>
      <div class="#" >
        <p>¥<span id="one_price">10,000</span></p>
      </div>
    <P>合計金額</P>  
      <div class="#" id="total_price">
        <p>¥0</p>
      </div>
  </div>

  <div class="#">
    <%= form_with(model: @event, url:confirm_events_path, local: true) do |f| %> #←ここは注意

      <div>
        <%= f.hidden_field :price, id: "f_price"  %> #←ここはhidden
      </div>

      <div class="#">
        <%= f.label :num, '参加人数' %>
          #ここからプルダウンにする記述#
          <% num_options = Num.order(:id).map { |c| [c.name, c.id, data: { multiple: c.multiple }] } %>
          <%= f.select(:num_id, num_options, {}, {class:"#", data:{select:0}, id:"num" }) %>
          #ここまで#
      </div>
      <div class="submit">
        <%= f.submit "submit", class:"#" %>
      </div>
    <% end %>
  </div>
</div>

<%= form_with(model: @event, url:confirm_events_path, local: true) do |f| %>
pathはrails routesで調べてください。今回は確認画面を挟んだ実装をしているので上記の様なpathになってます。

ポイントはプルダウンのoptionsデータ属性です。

項目 名前
変数名 num_options
data属性名 multiple
id num

としました。
ここまでが下地で続いてJavaScriptです。

2. javascrip

最初javascriptのapplication.jsファイルを編集します。

app/javascript/packs/application.js
//省略

require("@rails/ujs").start()
// require("turbolinks").start() ←ここをコメントアウト
require("@rails/activestorage").start()
require("channels")
require("../price") //←ここに追記

//省略

javascript配下新しいファイルを作成します。
今回はpriceという名前にします。

スクリーンショット 2020-12-02 12.59.16.png

まず型枠を記述します。

app/javascript/price.js
const price = function () {
  const onePrice = document.getElementById("one_price");
  const totalPrice = document.getElementById("total_price");
}

window.addEventListener("load", price);

一人当りの料金をone_price合計をtotal_priceとしてます。

ビューのプルダウンで人数を選択するとJavaScriptが発火する様に記述します。

app/javascript/price.js
  const selectNum = document.getElementById("num");
    selectNum.addEventListener('change', function (){

idがnumのdata属性multipleの値を取る。

app/javascript/price.js
      // numのdata属性の値を取る
      const numSelectBox = document.getElementById("num");
      const dataNum = numSelectBox.options[ numSelectBox.selectedIndex].getAttribute("data-multiple");

細かくなりますが、一人料金のidを取得したあと値を数列に変換して、さらにカンマを除去します。

app/javascript/price.js
      // 一人料金のidを取得
      const priceOne = document.getElementById("one_price");
      //値を数列にする
      const displayPriceOne = priceOne.innerHTML;
      //カンマを除去する
      const displayPriceOneInteger = Number( displayPriceOne.replace(/,/, '') );

計算して、表示ように変換して、表示します。

app/javascript/price.js
      // 計算する
      const total = (displayPriceOneInteger*dataNum);
      // 正規表現でカンマ区切りに直して日本円に変換する
      const totalComma = total.toLocaleString('ja-JP', { style: 'currency', currency: 'JPY'});
      // 表示する
      totalPrice.innerHTML = totalComma;

最後、ビューで表示した値をvalueプロパティでtextarea(今回はhidden_field)要素に値を代入します。

app/javascript/price.js
      // ----<フォームに入力>
      // 合計金額の値を取得 
      const displayPriceTotal = totalPrice.innerHTML;
      // 取得した合計金額をフォームに入力
      document.getElementById("f_price").value = displayPriceTotal;
    });

つなげると以下の様になります。

app/javascript/price.js
const price = function () {
  const totalPrice = document.getElementById("total_price");
  const onePrice = document.getElementById("one_price");

  // ------<人数を選択した時の計算>
  const selectNum = document.getElementById("num");
    selectNum.addEventListener('change', function (){
      // numのdata属性の値を取る
      const numSelectBox = document.getElementById("num");
      const dataNum = numSelectBox.options[ numSelectBox.selectedIndex].getAttribute("data-multiple");
      // 一人料金のidを取得
      const priceOne = document.getElementById("one_price");
      //値を数列にする
      const displayPriceOne = priceOne.innerHTML;
      //カンマを除去する
      const displayPriceOneInteger = Number( displayPriceOne.replace(/,/, '') );
      // 計算する
      const total = (displayPriceOneInteger*dataNum);
      // 正規表現でカンマ区切りに直して日本円に変換する
      const totalComma = total.toLocaleString('ja-JP', { style: 'currency', currency: 'JPY'});
      // 表示する
      totalPrice.innerHTML = totalComma;
      // ----<フォームに入力>
      // 合計金額の値を取得 
      const displayPriceTotal = totalPrice.innerHTML;
      // 取得した合計金額をフォームに入力
      document.getElementById("f_price").value = displayPriceTotal;
    });
}

window.addEventListener("load", price);

完成です!!

まとめ

以上、 JavaScriptとActiveHash(アクティブハッシュ )を使って、非同期で料金の自動計算をする方法でした。

最後に
私はプログラミング初学者ですが、同じ様に悩んでる方々の助けになればと思い、記事を投稿しております。
それでは、また次回お会いしましょう〜

参考

https://www.sejuku.net/blog/23998
https://techacademy.jp/magazine/22426
https://lealog.hateblo.jp/entry/2013/02/28/005010
https://lab.syncer.jp/Web/JavaScript/Snippet/29/

【Rails】ActiveHash(アクティブハッシュ)の導入と「確認」→「保存」→「表示」
https://qiita.com/AKI3/items/022ad68e22f798d40ad9

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

未経験でも大丈夫!FlowからstrictなTypeScriptへ移行

本稿は TypeScript Advent Calendar 2020 15日目の記事です!

はじめに

TypeScriptはJavaScriptを用いたアプリケーションをゼロから構築する際には選択しない手はないといっても過言ではありません!
ところが既存のアプリケーションにTypeScriptを導入したい場合、且つFlowを利用しているアプリケーションのケースではどのように移行していくべきか?

なかなかFlowや素のJSからTypeScriptへ移行できずにいるという方の為に、実際にJavaScript + Flow のアプリケーションで約300ファイルをTypeScriptに移行した手法を共有したいと思います!

タイトルには「Flowから」とありますが、プレーンなJavaScriptからの移行にも通ずる部分は多くあるはずです!

※移行を実施したのは2019年後半頃になります
※本稿はFlowを否定するものではありません

話さないこと

  • TypeScriptの基本的な導入方法
  • TypeScriptの書き方
  • 具体的なリプレイス前後のコード比較

TL;DR

  • 最初からany禁止にせずに段階的に禁止設定を有効にしていくことで素早く移行する
  • リファクタはせずに型定義の変更のみすることで既存機能への影響を与えないようにする
  • 一括置換やツールを使わずに手動で書き換えていくことで理解を深めていった

移行の方針

極力、型定義の変更のみに注力する!

既存のFlowによる型定義をTypeScriptでの型の表現に置き換えていくことだけを基本としました。

  • 型定義の変更のみであれば既存の機能への影響は最小限のはずである
  • コード全体を見直すとついリファクタしたい衝動に駆られますが、上記の理由からグッと堪える・・・!
  • 後でリファクタしたい箇所や気になって点はコメントで残したり、チームメンバーへ別途共有
  • まずはTypeScriptへの移行を素早く完了させる

コードレビューでTypeScriptを学んでもらう

自分を含めTypeScript未経験のメンバーもいる環境だったので、教育も兼ねたコードレビューを通して徐々に理解していってもらいました。

  • 一度に大量の差分を依頼するのではなくある程度キリのいい単位で区切る
  • 一つのプルリクに対して2名のレビュアーをアサイン
  • フロントエンドのメンバー全員が満遍なくコードレビューできるように

移行の方法

ソースコードは基本的に全て手動で変更していきました・・・!

CLIやIDEの機能で置換したり、FlowからTypeScriptへのマイグレーションツールもある中、敢えて手作業で実施した理由は
前述した通りTypeScriptはほぼ未経験なので自分の手で書き換えていく方がTypeScriptへの理解が深まると考えたからです。

TypeScriptへの理解は充分でサクッと移行してしまいたい場合はわざわざ手動で疲弊する必要もないのかなとは思います!

手順

TypeScriptの移行と並行して機能の開発が進んでいるので、一気に完全TypeScript化するのではなく以下の手順で進めました!

  1. TS, JSが共存できる状態でmasterへマージ
  2. 各開発ブランチでmasterブランチの取り込み -> TS化対応
  3. 共存用の設定を破棄し、TSに完全移行
  4. any型の駆逐

tsconfig.jsonの設定

まずはtsconfig.jsonの設定ですが、TSとJSを共存させる為に以下の様にしました。

  • TSからJSをimportすることを許容させるためにtsconfig.jsonではallowJstrue
  • Flowで明示的な型指定がされていない部分が多々あったため、noImplicitAnyfalse
    • 型指定がなく暗黙的にanyに推論されてしまう箇所を全て対応していくと非常にコストがかかる
    • まずはTSへの移行を優先し、後にanyを駆逐していく方針を取った
  • stricttrue
    • 強くおすすめします!
    • noImplicitAnyはこの時点では移行の速度重視で止むを得ずfalseにしていますが、他の設定を後からtrueにしてくのはなかなか厳しいかと・・・
tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "strict": true,
    "noImplicitAny": false, // strictをtrueにするとnoImplicitAnyもtrueに設定されるので明示的にfalseを指定
    // ...
  }
}

参考: TypeScript: TSConfig Reference - Docs on every TSConfig option

ESLintの設定

Flow用のESLintルールとTypeScript用のESLintルールをoverrides設定を利用し
拡張子.js.tsでそれぞれで対応するPlugin等の適用を行いました。

.eslintrc.js
module.exports = {
  // ここまで .js, .ts共通のルール
  overrides: [
    {
      files: ['*.js'],
      extends: [
        'plugin:flowtype/recommended',
      ],
      plugins: [
        'flowtype-errors',
      ],
    },
    {
      files: ['*.ts'],
      extends: [
        'plugin:@typescript-eslint/recommended',
      ],
      plugins: [
        '@typescript-eslint',
      ],
      parser: '@typescript-eslint/parser',
      // ...
    },
  ],
};

※overridesでのextends指定はESLint v6から可能です!

エラーが解決できない場合

繰り返しになりますが、初期段階ではまずはTypeScriptへの移行を優先させたかったという背景があるのと
TypeScriptにも慣れ、型パズル力が向上したときに向き合うことを前提として一時的にエラーを回避しました。

これから紹介するエラー回避手段は決して推奨されるものではありません!

noImplicitAny

前項のtsconfig.jsonの設定にて記述した通り、暗黙的なany型を一時的に許容

// 型アノテーションがないのでargはanyで推論される
function foo(arg) {
  bar(arg);
}

明示的なany型の許容

@typescript-eslint/no-explicit-any

  • 明示的なany型の指定に関するESLintのルール
  • 無効化しておくことでany型を使ってもESLintエラーにならない
  • どうしても上手く型を定義できないときはanyを指定して逃げる
function foo(arg: any) {
  bar(arg);
}

TSエラーの許容

@ts-ignore

  • TypeScriptの型エラーを許容させる
  • 利用しているライブラリ側の型定義と整合性が取れない場合など(それはそれで問題なのですが・・・)
  • @typescript-eslint/ban-ts-commentを利用し、オプションで'ts-ignore': 'allow-with-description'を指定しておくと同一行内にコメントがある場合のみ許容させることができる
function foo(arg: string) {
  // @ts-ignore barはstringを許容していない
  bar(arg);
}

Double assertion

Double assertion

  • TypeScriptにはアサーションという機能があり、ある型をコンパイラに対して明示的に別の型として認識させることができる機能
  • アサーションは基本的に型の互換性のある時にのみ使えるテクニックですが、Double assertionを使うことでどんな型でも任意の型として指定することができてしまう
  • @ts-ignoreが次の行に対する型エラーを全て許容させるのに対し、こちらはあくまで型アノテーションとして部分的に型エラーを回避させることが可能
function foo(arg: string) {
  bar(arg as unknown as number);
}

移行対応完了後

Flowの設定削除

今までお世話になったFlow関連ファイルや設定をおもむろに削除していきましょう!
eslintrc.jsのoverridesからFlowの設定を削除するのと合わせて、非有用に応じて.ts用の設定はoverridesの外に移動しましょう。

any型の駆逐

駆逐すべきany型は二種類です!

  • 暗黙的なany
    • tsconfig.jsonのnoImplicitAnyオプションを有効化(strictがtrueの場合はnoImplicitAnyの行ごと削除)
  • 明示的なany
    • @typescript-eslint/no-explicit-anyルールを有効化

それぞれIssueを立て、ディレクトリ単位などで各メンバーが自主的に取り組めるようにしました。
ここでも型以外は修正しないことというルールを厳守することで機能への影響が出ないようにしました。

楽しく対応できたらいいなーと↓の様なIssueを立てて対応していましたw

image.png

TypeScriptへ移行して

  • 型表現がFlowに比べて非常に柔軟に定義でき、TypeScriptが使える環境であれば今後Flowを使うことはなさそう
  • 先日発表されたGitHubのOctoverseでもTypeScriptが4位に急上昇するなど、非常に勢いのある言語を利用できているという開発者体験・満足感
  • 厳格な型チェックによるコードの安心感を得ることができた
  • TypeScriptなしでは生きていけない!

おわりに

Flowからの移行というタイトルで執筆しましたが、今回お伝えしたかった点としては
一度に全てをやりきるのではなく段階的に移行することでメンバーの言語に対する理解もエンハンスしつつ
素早くアプリケーションに適用していくことができたという内容でした!

少しでもTypeScriptへの移行を検討するきっかけと手助けになればと思います!

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