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

Javascriptを使って、白背景の楽譜の背景を透明にする

はじめに

ドラム譜を作成しようと思いMuseScoreというのを使って楽譜を作成したんですが、書いた楽譜のスクリーンショットを撮るとこんな感じになります

basic4beat.png

こういう白背景(厳密には真っ白ではないが)の楽譜の背景を透明にしたい場合、いろいろな方法があると思いますが今回はJavascriptを使って行ってみました。

最終的に背景を透明にした画像はこの様に重ねて使えるようになります。

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

使うもの

  • mac
  • terminal
  • javascript
  • node.js
  • yarn
  • pngjs

上についてわからないものがあれば、Googleで調べてから再度ここにきてください。僕の過去の記事でも説明しているものがあると思うので参考にしてみてください。

書いたコード

const fs = require('fs')
const PNG = require('pngjs').PNG

fs.createReadStream('path/to/your.png')
    .pipe(new PNG())
    .on("parsed", function() {
        for (let y = 0; y < this.height; y++) {
            for (let x = 0; x < this.width; x++) {
                const index = (this.width * y + x) << 2;
                const r = this.data[index]
                const g = this.data[index + 1]
                const b = this.data[index + 2]
                if (r + g + b > 255 / 2 * 3) {
                    this.data[index + 3] = 0
                }
            }
        }

        this.pack().pipe(fs.createWriteStream('path/to/your/output.png'))
    })

解説

まずはpngjsのドキュメントを読みます。

最初からゴールが見えた様な状態のサンプルが書いてあったので、それを使いますが、前提知識として。

我々が普段使っているディスプレイは、1ピクセルという単位があり、1つのLEDのようなものです。例えばFullHDのディスプレイだと横に1920個・縦に1080個並んでいて、これらの色加減によって我々は画像を認識しています。

PNGの場合、1ピクセルは赤(red)・緑(green)・青(blue)・透明度(alpha)と言う4種類の単位で表現されます。rgbaとか言ったりしますね。

そして、データ上は先頭から[1ピクセル目のr][1ピクセル目のg][1ピクセル目のb][1ピクセル目のa][2ピクセル目のr][2ピクセル目のg]...という様に配列に格納されています。

それぞれ0~255までの256個の整数で構成されています。

ただこれだと全てのデータが横一列に並んでいるため、二次元の画像として使うためには高さ(width)と横幅(height)の情報が必要です。これもPNGファイルに含まれていて、その辺りはpngjsがうまいこと読み込んで処理してくれているのでこちらが考えることはrgbaデータをどの様に加工するかです。

ここまでが前提知識で、今回やりたかった白背景を透明にする部分ですが、途中にif (r + g + b > 255 / 2 * 3) {と書いた行があり、この条件式は白に近い色を判定するためのもので、この後の行this.data[index + 3] = 0の部分が透明にする処理です。

あとはサンプル通りですがファイルパスを正しく書き換えれば出力してくれます。

おわりに

正直このコード書くより記事書く方が1000倍大変です。。。

ちなみにですが、この条件式と背景処理だと、出力される画像が若干ジャギーになってしまいます(丸みを帯びた物(音符)を表現するために、丸い部分は曖昧な色が使われているため)。

なので、もっと滑らかに透明にしたい場合は条件式と後処理にもっと手を加える必要がありますね。

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

スクワットしないとジェイソン・ステイサムに撃たれるLINE bot ~SQUAT or DIE~

Qiitaで「ジェイソン・ステイサム」というタグを密かに育んでいるたわちゃんです。
もちろん、今回も愛しのステイサムたんがテーマ!
ぜひ、最後までご覧ください♪

今回のステイサム作品

自粛により13キロも太ってしまった私。
空いた時間にスクワットしたいけどヤル気が出ないので、スクワットしないとステイサムに殺●れるLINE botを作りました。
その名も「SQUAT or DIE」です。

出来たもの

※キャプチャ撮る時LINE botの名前変えるの忘れてた・・・「SQUAT or DIE」です。
Image from Gyazo

基本、私からの問いかけには一切無視というドS設定ですが、スクワット表明をすると反応してくれます。

ステイサムたんの命令は絶対・・・!すぐにスクワット開始です。
ちなみにTシャツに反応した方は、そっとコメントください。

スクワット20回をやり切ると、ステイサムたんからご褒美をいただけます。

Image from Gyazo

逆に諦めると、撃たれます

Image from Gyazo

作り方

動画でもチラ見えしていますが、Obnizでスクワットの回数をカウントし、LINE Messaging APIを使ってLINE botと連携しています。ソースコードは記事の下部に貼ってるので、ここでは割愛しますね!

Obnizを跨いでスクワットし、超音波距離センサ(HC-SR04)でお股との距離を測り、50㎝以下になるとカウントするようにしました。
※ちなみに、超音波だからかお股だとなかなか反応してくれず、お股に手のひらをかざすとカウントしてくれました。笑

ちゃんとディスプレイの文字が見えるように、何度もスクワットしながら撮影したので筋肉痛になりました。

ソースコードと改善点

コチラ▶https://gist.github.com/twtjudy1128/05972d30e240979338476d3bc2eef82f

プロトアウトスタジオの先輩の記事
「onizで腹筋カウンター&line bot」https://qiita.com/karu/items/87e79eeb3c3a0892d00a を参考にさせていただきました!

ただ、自分でコード解読できないままお借りしたので、思い通りにカスタムできず、、、
while文の無限ループから抜け出せませんでした(´;ω;`)

<< 改善したい点 >>
▶ 回数もLINE BOTに表示させたい
▶ 20回スクワットしたら、自動的にステイサムから褒められたい
▶ 5分以内に20回スクワットしなかったら、自動的にステイサムに撃たれたい
▶ あと9キロ痩せたい

こう書き換えたらいいよ!とかアドバイスいただけると嬉しいです・・・

おまけ

実は!先日初めてハッカソンに参加しました!
そのテーマが「チーム制でObnizを楽しもう」だったのですが、チームで役割分担したら、一切Obnizを触ることなくハッカソンが終わっていました(笑)
(ハッカソンの様子はnoteにまとめてます▶https://note.com/tawata_judy/n/n7b5ac3b60e69 )

というわけで、自分でObniz使って何か作ってみよう!と思いチャレンジした次第です。
初めてObniz使った時も不完全燃焼だったのに、今回も中途半端になって悔しい~~~精進します!

参照▶【はじめてのIoT】サイリウムを自動的に光らせてみた【ラブライブ】
https://qiita.com/twtjudy1128/items/6df974ea12646665c73b

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

【javascript】Mutation observerを使ってみる

動的な追加要素を検知していろんな処理ができるMutation observerについてまとめる。

これがあれば快適なネットサーフィンがで着ると思う。
例えばgifばかりの動画はブラウザが重くなるので、ファイルがgifならimgタグごと消してしまうことで軽くなる

そういうことができるMutation observerについて軽くまとめてみる

Mutation observer

MutationObserver とは、指定したコールバック関数を DOM の変更時に実行させる API です。この API は、DOM3 Events の仕様で定義されていた Mutation Events を新しく設計し直したものです。

出典:https://developer.mozilla.org/ja/docs/Web/API/MutationObserver
つまりは監視。
追加/削除された要素をいじれる

インスタンス

MutationObserver(
  function callback
);

callbackの第1引数にMutationRecord
第2引数にMutationObserver インスタンス自身を受け取る

監視の開始 observe

void observe(
  Node target,
  MutationObserverInit options
);
引数 説明
target DOM変更を検したいNode
options 取得したい変更の種類を指定

optionに指定できるもの

プロパティ 説明
childList 対象ノードの子ノードでの追加・削除を監視
attributes 対象ノードの属性変更の監視
characterData 対象ノードのデータ監視
subtree 対象ノードとその子孫ノードの監視
attributeOldValue 対象ノードの変更前の属性値を記録
characterDataOldValue 対象ノードの変更前のデータを記録

これらのオプションをオブジェクトで渡す。

MutationRecordオブジェクト

オプションを説明したところで、MutationRecordが持つプロパティについて説明する。
オブザーバーのインスタンス時に書くが、変化があるとこのコールバック関数に渡される。

プロパティ 説明
type 変化の種類。childList,attributes,charactorDataか
target 変化したノード
addedNodes 追加されたノード
remocedNodes 消されたノード
previousSibling/nextSibling 追加/削除されたノードの前後にあるノード

監視の停止 disconnect()

observe()を再び呼び出すまでコールバック関数は実行されない

使い方

以下はこちらのサイトで挙げられている使用例

// 対象とするノードを取得
const target = document.getElementById('some-id');

// オブザーバインスタンスを作成
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    console.log(mutation.type);
  });    
});

// オブザーバの設定
const config = { attributes: true, childList: true, characterData: true };

// 対象ノードとオブザーバの設定を渡す
observer.observe(target, config);

// 後ほど、監視を中止
observer.disconnect();

解説

対象とするノードを取得

監視したいノードを取得する

オブザーバインスタンスを作成

ここで、変化した要素に何をするかを書く

オブザーバの設定

ここで変化した要素の種類を指定する

対象ノードとオブザーバの設定を渡す

ここで実際に実行

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

新型コロナの蜜対策を画像処理でやってみる

コロナ禍の悩み

会社のミーテイングなどの際、換気をしてるとはいえ、人数多くないと思ったことはありませんか?私はあります。しかももう集まってるし、言い出しづらい。

ハッカソンで挑戦する

2020年8月8日に初めてハッカソンに挑戦しその時のアイデア発散で出たアイデアから、一緒にチームで戦った@canonno君と以下のものを作りました。

密を見つけて走り出す!「ミツかるよんく」

obnizを2台使用しています一台は距離センサを2個使用して入退室管理、もう一台のobnizでラジコンを動かしています。

アイデア発散ボード

Image from Gyazo
ハッカソンでの私の発散ボードです。参加者の皆さんに貼っていただいたキーワードにYOLOなどでの学習済みモデルでの人間検出とありました、ハッカソンでは距離センサで入室人数を管理しましたが、今回は発展させYOLOの学習済みモデルで蜜を見つけてみたいと思います。

コード(未完)

index.html
<html lang="en">
<head>
  <meta charset="UTF-8">

  <title></title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script>
  <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js" type="text/javascript"></script>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
  <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>

  <script src="https://unpkg.com/obniz/obniz.js"></script>

</head>
<body>
  <div id="obniz-debug"></div>
  <h1></h1>
  <p id="status">Loading Model...</p>
  <script>
let video;
let detector;
let detections;
let bodyPix;

  var obniz_id = document.getElementsByName("obniz_id");
  var obniz = new Obniz(obniz_id);


  obniz.onconnect = async () => {
  let speaker = obniz.wired("Speaker", { signal: 0, gnd: 4 });

  }
}

function setup() {
  createCanvas(480, 360);

  video = createCapture(VIDEO);
  video.size(width, height);
  video.hide();

  detector = ml5.objectDetector('yolo', modelReady)
  console.log(detector);

}

function modelReady() {
  console.log('model loaded')
  detect();
}

function detect() {
  detector.detect(video, gotResults);
}

function gotResults(err, results) {
  if (err) {
    console.log(err);
    return
  }
  // console.log(detector);
  detections = results;

  detect();
}

function draw() {
  image(video, 0, 0, width, height);

  if (detections) {
    detections.forEach(detection => {
      noStroke();
      fill(255);
      strokeWeight(2);
      text(detection.label, detection.x + 4, detection.y + 10)

      noFill();
      strokeWeight(3);
      if (detection.label === 'person') {
        stroke(0, 255, 0);
        if (detection.label === 'person') {
        speaker.play(1000);
        console.log(detection.label);
      } else {
        stroke(0, 0, 255);
      }

      rect(detection.x, detection.y, detection.width, detection.height);
    })
  }
}
  </script>
</body>
</html>

動かない

Ml5.jsからYOLOのサンプルコードを引っ張ってきて、取り敢えず人間を検出する所まで行きました、続いてそのプログラムの中にobnizにコネクタできるコードを加えて、人を検出したらスピーカを鳴らすコードを作りました、で試そうと思いましたがうまくいきません。人間を検出したらエラーがでてしまいます。エラーを見て解決を試みますが、うまくいかず・・・今後の課題です。

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

React Tutorial 「改良のアイデア」の実装

React Tutorial

React公式のチュートリアルページです。

三目並べゲームを一通りハンズオンで実装します。
その後、改良のアイデアを提案されます。

JavaScriptの練習がてら挑戦してみました。

React Tutorial

時間がある場合や、今回身につけた新しいスキルを練習してみたい場合に、あなたが挑戦できる改良のアイデアを以下にリストアップしています。後ろの方ほど難易度が上がります:

1. 履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。
2. 着手履歴のリスト中で現在選択されているアイテムをボールドにする。
3. Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。
4. 着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。
5. どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。
6. どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

ここから続きを実装しました。

ソースコード

開く
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import classNames from "classnames";

function Square(props) {
  const squareClass = classNames("square", { "high-light": props.winnerSquare === 0 || props.winnerSquare });
  return (
    <button className={squareClass} onClick={props.onClick}>
      {props.value}
    </button>
  );
}

class Board extends React.Component {
  renderSquare(i) {
    const winnerSquare = this.props.winnerSquares && this.props.winnerSquares.includes(i) ? i : null;
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
        winnerSquare={winnerSquare}
      />
    );
  }

  render() {
    const boardRows = [];
    let k = 0;
    for (let i = 0; i < 3; i++) {
      const threeSquares = [];
      for (let j = 0; j < 3; j++) {
        threeSquares.push(this.renderSquare(k++));
      }
      boardRows.push(<div className="board-row">{threeSquares}</div>);
    }
    return <div>{boardRows}</div>;
  }
}

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null),
          player: null,
          col: 0,
          row: 0,
        },
      ],
      stepNumber: 0,
      xIsNext: true,
      choosedHistory: null,
      isAsc: true,
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares)[0] || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    const [col, row] = this.convertColAndRow(i);
    this.setState({
      history: history.concat([
        {
          squares: squares,
          player: squares[i],
          col: col,
          row: row,
        },
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
      choosedHistory: null,
    });
  }

  convertColAndRow(i) {
    const colAndRow = [];
    for (let i = 1; i <= 3; i++) {
      for (let j = 1; j <= 3; j++) {
        colAndRow.push([j, i]);
      }
    }
    return colAndRow[i];
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      choosedHistory: step,
      xIsNext: step % 2 === 0,
    });
  }

  changeOrder() {
    this.setState({
      isAsc: !this.state.isAsc,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const [winner, winnerSquares] = calculateWinner(current.squares);
    console.log(winnerSquares);

    const moves = history.map((step, move) => {
      const desc = move ? "Go to move #" + move : "Go to game start";
      const info = step.player ? `player: ${step.player}, col: ${step.col}, row: ${step.row}` : "";

      const button = (i) =>
        <button className={move === this.state.choosedHistory ? "bold" : ""} onClick={() => this.jumpTo(i)}>{desc}</button>

      return (
        <li key={move}>
          {button(move)}
          <span>{info}</span>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

    let drawMessage;
    if (!winner && !current.squares.includes(null)) {
      drawMessage = "The result is a draw.";
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board squares={current.squares} onClick={(i) => this.handleClick(i)} winnerSquares={winnerSquares} />
        </div>
        <div className="game-info">
          <div>
            {status}
            <button className="toggle-btn" onClick={() => this.changeOrder()}>
              {this.state.isAsc ? "Desc" : "Asc"}
            </button>
            <span>{drawMessage}</span>
          </div>

          <ol className={this.state.isAsc ? "asc-list" : "desc-list"}>{moves}</ol>
        </div>
      </div>
    );
  }
}

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

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

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return [squares[a], lines[i]];
    }
  }
  return [null, null];
}
index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

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

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

.status {
  margin-bottom: 10px;
}

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

.square:focus {
  outline: none;
}

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

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

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

span {
  margin-left: 6px;
}

.bold {
  font-weight: bold;
}

.toggle-btn {
  margin-left: 10px;
  color: white;
  background-color: black;
  border: none;
  border-radius: 7px;
}

ol {
  display: flex;
}

ol.asc-list {
  flex-direction: column;
}

ol.desc-list {
  flex-direction: column-reverse;
}

.high-light {
  background-color: yellow;
}

画面

ezgif.com-crop.gif

懸念点

this.props.winnerSquares && this.props.winnerSquares.includes(i) ? i : null;
props.winnerSquare === 0 || props.winnerSquare

もう少しいい書き方がありそう。

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

ポリモーフィズムへの関数型アプロ―チ

ポリモーフィズムとは一言でいうと同じコードの中の同じ変数が、実際の値に応じて異なる振る舞いをすることを言います。
たとえば a + b という式が行う処理は、 a と b が数値型かベクトルかによって変化するような性質のことです。

特にオブジェクト指向におけるポリモーフィズムとは、仮想関数を継承&オーバーライドすることによってオブジェクトの実体ごとに異なる挙動を持たせるということを意味することが多いです。

オブジェクト指向

まず、オブジェクト指向でもって普通に継承とオーバーライドで実装してみます。
console.log("behaving...") の部分は、実際にはめちゃくちゃ長いコードで、繰り返し書きたくない、なんて場合を想像してください。

class Animal{
  behave(){
    console.log("behaving...")
    this.cry()
  }
}

class Cat extends Animal{
  cry(){
    console.log("meow")
  }
}

class Dog extends Animal{
  cry(){
    console.log("bow wow")
  }
}

const cat = new Cat()
const dog = new Dog()

cat.behave()
dog.behave()

関数型?

関数型アプローチではこのようになります。
めちゃくちゃ短いですね。

const behave = cry => () => {
  console.log("behaving...")
  cry();
}

const cat = behave(() => console.log("meow"))
const dog = behave(() => console.log("bow wow"))

cat()
dog()

複数のクラス階層

ここで人間に登場してもらって、2足歩行する動物として legs() メソッドで 2 を返してもらいます。
4足歩行する犬と猫は共通の QuadrupedalAnimal を継承して legs() メソッドを共有してもらいましょう。

class Animal{
  behave(){
    console.log("behaving...")
    this.cry()
    console.log(`walking with ${this.legs()} legs...`)
  }
}

class QuadrupedalAnimal extends Animal{
  legs(){
    return 4;
  }
}

class Cat extends QuadrupedalAnimal{
  cry(){
    console.log("meow")
  }
}

class Dog extends QuadrupedalAnimal{
  cry(){
    console.log("bow wow")
  }
}

class Human extends Animal{
  cry(){
    console.log("boo hoo")
  }
  legs(){
    return 2;
  }
}

const cat = new Cat()
const dog = new Dog()
const human = new Human()

cat.behave()
dog.behave()
human.behave()

こんな単純なことをするのにやたら長いですね。

関数の部分適用

関数型なら部分適用によって中間層のクラスに相当するものを作るのも容易です。

const behave = (cry, legs) => () => {
  console.log("behaving...")
  cry()
  console.log(`walking with ${legs()} legs...`)
}

const quadrupedalAnimal = cry => behave(cry, () => 4)
const cat = quadrupedalAnimal(() => console.log("meow"))
const dog = quadrupedalAnimal(() => console.log("bow wow"))
const human = behave(() => console.log("boo hoo"), () => 2)

cat()
dog()
human()

メソッドに名前がついてないと、引数が増えてきたときに順番がごっちゃになるのではないかと心配する向きには、簡単に名前を付ける方法があります。

const behave = ({cry, legs}) => () => {
  console.log("behaving...")
  cry()
  console.log(`walking with ${legs()} legs...`)
}

const quadPedalAnimal = ({cry}) => behave({cry, legs: () => 4})
const cat = quadPedalAnimal({cry: () => console.log("meow")})
const dog = quadPedalAnimal({cry: () => console.log("bow wow")})
const human = behave({cry: () => console.log("boo hoo"), legs: () => 2})

cat()
dog()
human()

オブジェクトを返す

え、欲しいのは関数じゃなくて、複数のメソッドやフィールドを持ったオブジェクトのような振る舞いをするものですか?
まあ、別に問題ありません。

const animal = ({cry, legs}) => ({
  behave: () => {
    console.log("behaving...")
    cry()
  },
  walk: () => console.log(`walking with ${legs()} legs...`)
})

const quadPedalAnimal = ({cry}) => animal({cry, legs: () => 4})
const cat = quadPedalAnimal({cry: () => console.log("meow")})
const dog = quadPedalAnimal({cry: () => console.log("bow wow")})
const human = animal({cry: () => console.log("boo hoo"), legs: () => 2})

cat.behave()
cat.walk()
dog.behave()
dog.walk()
human.behave()
human.walk()

これはファクトリ関数パターンとしてよく知られているものですね。

猫や犬のファクトリが欲しいけど、名前だけカスタマイズしたい?下の catWithNamedogWithName のようにできます。

const animal = ({name, cry}) => ({
  name,
  behave: () => {
    console.log(`${name} behaving...`)
    cry()
  },
})

const namedAnimal = params => name => animal({name, ...params})
const catWithName = namedAnimal({cry: () => console.log("meow")})
const dogWithName = namedAnimal({cry: () => console.log("bow wow")})

const cat = catWithName("Michel")
const dog = dogWithName("Taro")
cat.behave()
dog.behave()

さらにクレイジーな例として、名前は先に Michel にするって決めているけど、どんな動物かは後で決めたい、なんて場合は次の animalNamedMichel みたいなのでどうでしょう。

const animal = ({name, cry}) => ({
  name,
  behave: () => {
    console.log(`${name} behaving...`)
    cry()
  },
})

const animalNamed = name => animal => animal(name)
const animalNamedMichel = animalNamed("Michel")
const namedAnimal = params => name => animal({name, ...params})
const catWithName = namedAnimal({cry: () => console.log("meow")})
const dogWithName = namedAnimal({cry: () => console.log("bow wow")})

const cat = animalNamedMichel(catWithName)
const dog = animalNamedMichel(dogWithName)
cat.behave()
dog.behave()

まぁあまりやりすぎると可読性を損なうのでほどほどにしておくべきだと思いますが、関数型アプローチが JavaScript のオブジェクトモデルと相まって、どれだけコードを簡素にしうるかの片鱗を垣間見ることができると思います。

C++の場合

C++のような静的型付け言語でもジェネリックラムダを使えば関数型ポリモーフィズムが実現できます。 JavaScript ほどは簡潔には書けませんが。

#include <iostream>

int main(){
    auto behave = [](auto cry){
        return [cry](){
            std::cout << "behaving..." << std::endl;
            cry();
        };
    };

    auto cat = behave([](){ std::cout << "meow\n"; });
    auto dog = behave([](){ std::cout << "bow wow\n"; });

    cat();
    dog();

    return 0;
}

注意点としては、この場合の catdog という変数は別の型を持つので、同じ型を期待する関数の引数や配列に一緒に入れることはできません。
どうしても必要な場合は std::function でくるんでやる必要があります。これは動的メモリを使用するということを意味しますので、オブジェクト指向的ポリモーフィズムの仮想関数に比べてパフォーマンス面でハンデになるかもしれません。

テンプレートを使う

さらには、 std::function も仮想関数テーブルのオーバーヘッドも支払いたくないというパフォーマンスフリークの皆様方には、テンプレートによる静的ポリモーフィズムがございます。

#include <iostream>

template<typename Cry>
struct Animal{
    void behave(){
        std::cout << "behaving..." << std::endl;
        Cry::cry();
    }
};

struct CatCry{
    static void cry(){
        std::cout << "meow\n";
    }
};
struct DogCry{
    static void cry(){
        std::cout << "bow wow\n";
    }
};

int main(){
    auto cat = Animal<CatCry>();
    auto dog = Animal<DogCry>();

    cat.behave();
    dog.behave();

    return 0;
}

しかし、これはラムダ式と同様に全く異なる型となってしまい、同じ制約に縛られます。最適化をかければジェネリックラムダと同じ機械語に翻訳される可能性も高いです。ジェネリックラムダが使えるコンパイラと状況であれば、そちらを使ったほうがよいでしょう。

Rust の場合

Rust の場合は入れ子になったラムダ式を書くのが若干 C++ より楽です。
ただし、ボローチェッカーのお導きにより、メソッドは内側のクロージャにムーブしてもらう必要がありますので、 move キーワードがネストされたラムダ式の間に入ります。

fn main(){
    let behave = |cry: fn()| move || {
        println!("behaving...");
        cry();
    };

    let cat = behave(|| println!("meow"));
    let dog = behave(|| println!("bow wow"));

    cat();
    dog();
}

実際役に立つ場面

実際にはデータモデルの定義よりは、込み入ったロジックの繰り返しを減らすときに役に立つ気がします。
例えば下記のようにほとんど同じだけどちょっとだけ対象のオブジェクトに応じてカスタマイズしたい部分がところどころにあるコードを繰り返し適用したい場合、

for(let object in objects){
  いろいろ込み入ったことをする(object)

  ちょっとだけカスタマイズしたい部分(object)

  もっといろいろ込み入ったことをする(object)

  もうちょっとだけカスタマイズしたい部分(object)
}

次のような関数を定義しておくと便利です。

const 繰り返す部分 = (ちょっとだけカスタマイズしたい部分,
                      もうちょっとだけカスタマイズしたい部分) =>
                     (object) =>
{
  いろいろ込み入ったことをする(object)

  ちょっとだけカスタマイズしたい部分(object)

  もっといろいろ込み入ったことをする(object)

  もうちょっとだけカスタマイズしたい部分(object)
}

このためだけに基底クラスを定義して、派生クラスを実装して…なんて美しくないですよね。
これも広い意味でのポリモーフィズムです。

あとがき

オブジェクト指向がもてはやされなくなってきて久しいですが、伝統的なオブジェクト指向の代表的な使い道である継承とポリモーフィズムについても、関数型アプローチで結構簡単に実現できてしまいます。
特にJavaScript(およびその派生言語)の文法は非常に簡潔で、伝統的なオブジェクト指向で書かれた同じロジックなど冗長すぎて見る気も失せてきます。

あえてオブジェクト指向のいいところもフォローをしておきますと、カプセル化の概念は関数型のアプローチで実現するのは難しいです 1

また、関数型ではリフレクションや RTTI のような機能は一切使えません。 instanceof なんかで継承関係を調べることはできません。


  1. まあそうは言ってもメンバフィールドに公開せずにファクトリ関数の引数としてクロージャにキャプチャさせるだけにしておけば、オブジェクト構築以降、外からは触れない変数はプライベート変数っぽくなりますけどね。 

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

自分用メモ ライフサイクル

はじめに

ライフサイクルについてインプットしたのでアウトプットしておきます!!
reactを学び始めてこのライフサイクルが一番自分にとって難しく理解に苦しんでいます!!
まだまだ理解の途中ですが、アウトプットして行きながら理解を深めておきたいと思います!!

至らない点など数あると思いますが、アドバイスしていただけると幸いです!!

ライフサイクル

これはreactで学ぶ上で最初に教えてもらったことで、これであればなんとなく理解できました!

mounting

・constructor
・render
・componentDidMount
UI(ユーザー表示)のための準備
これらが主要メソッドで上からの順でメソッドが呼ばれる!!

updating

・render
・componentDidUpdate
ユーザーの処理などによりUIを更新、変化行う
ユーザーが操作できる箇所
ここの間はstate,propsが更新変更されるたびに何度もサイクルされる!!

unmounting

・componentWillUnmount
他のコンポーネントに切り替えたりする時に現在のコンポーネントを削除する箇所

注意)すいません!今記述した以外にもメソッドは多くあります。今自分がなんとか理解できてるメソッドのみ書かせていただきました。
詳しく知りたい方は是非こちらを見ていただくようお願いいたします!

ライフサイクルメソッド 

ユーザー表示するまでのサイクル

この中で必ず使うのはrenderのみになるので使わないのであれば使わなくてもいい!!

componentDidMount

mountingの時に呼び出されるメソッド
renderの後に呼ばれ、1回のみ呼ばれるメソッド
データを取ってくる場合や、タイマーやアニメーションをつけたりする時に使う!

render

mounting/updateの時に呼ばれるメソッドで一番呼ばれ、唯一必ず記載しなければならないメソッド!!
render内のコードがUI(ユーザー表示)になるもの
props,stateが変更されるたびに呼び出されるものになる!!
注意)renderの中で直接state,propsの変更を行ってはいけない!してしまうとrender内ではstate,propsを変更するとメソッドが呼ばれるのでrenderが無限にループされてしまう!!

componentDidUpdate

updateの時に使う
コンポーネント更新後に行われるメソッド
props,stateが変更時に呼び出される!!
多く呼び出されるメソッドなのでできるだけstate,propsの更新を行う思い処理は書かない!!

componentWillMount

unmountの時に呼ばれるメソッド
アニメーションやタイマーをセットしていた場合ここで解除する
リソースの解除を行う時に他のコンポーネントに影響を及ぼさないようにする!
ここでprops,stateの更新、変更はできない!unmount時はrenderが呼ばれないから!!

終わりに

以上が主要のメソッドになるのですが、他にも多くメソッドは存在しているため、必要になった場合勉強してここでも追加していこうと考えています!!

そしてこれらの主要メソッドの代替方法としてreact hooks(useEffect)があるのですが、こちらについては別記事で書いております!!
私自身最初にこのサイクルをきちんと理解せずにhooks(useEffect)を使っていたのでそのありがたみを全く理解できていませんでした。
これからもおそらく便利なhooksを利用したいと思いますが、このサイクルとメソッドを構造的に理解していないとreactの深い理解はできませんのでライフサイクルについてはもっともっと理解を深めて行きたいと思います!!

参照

React(v16.4) コンポーネントライフサイクルメソッドまとめ
React.Component

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

Ajvを使ってJSONのバリデーションをする(JavaScript/TypeScript)

環境

はじめに

Ajv はブラウザ環境でも使用できますが、ここでは NodeJS(TypeScript)で使った場合の例になります。
githubページ に必要十分な情報はありますが、分量が多いので、ここでは特に必要な部分や実際のスキーマ記述例をまとめています。

ブラウザで使う場合の参照先(github)

Ajv の特徴

 ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'))
  • 他の同種のライブラリより高速(参照先)。
  • ajv-18n を使うと、エラーメッセージの多言語化も対応可能です。

使い方

import Ajv from 'ajv'
import schema from 'somewhere/schema.json'

const ajv = new Ajv()
const validate = ajv.compile(schema)

const valid = validate(data)
if (!valid) console.log(validate.errors)

schema ファイルの例

JSON ではコメントは使えませんが、ここでは説明のために //〜 としてコメントを追加して、且つ改行を入れています。

{
  // この値は他のschemaファイルから参照される時に使われるだけなので、
  // 実際にこのURLにschemaファイルを置いてダウンロード出来るようにする必要はありません。
  "$id": "http://example.com/schemas/schema.json",

  // この設定がない場合、draft 6 meta-schema に対してバリデーションが行われます。
  "$schema": "http://json-schema.org/draft-07/schema",

  "title": "Your title comes here",
  "description": "Your description comes here",
  "type": "object",

  // これにより、定義していないフィールドがあった場合にエラーとします。
  // 尚、定義されたレイヤーでしか有効ではありません。
  // 階層化された下位層の object に対しては、それ毎に定義が必要です。
  // 基本的に、"type": "object" の後につけたほうがいいでしょう。 
  "additionalProperties": false,

  // definitions に定義した値は、$ref としてスキーマ内から参照可能です(後述)。
  "definitions": {
    "languageStrings": {
      "description": "This is an example of definition",
      "type": "object",
      "minProperties": 1,
      "examples": [
        {
          "en": "English title",
          "ja": "日本語タイトル"
        }
      ],
    }
  },

  // properties にフィールドの定義をしていきます。
  "properties": {
    "$schema": {
      "type": "string"
    },
    "stringExample": {
      "description": "This is an example of string type",
      "type": "string"
    },
    "numberExample": {
      "description": "This is an example of number type",
      "type": "number"
    },
    "arrayExample": {
      "description": "This is an example of array type",
      "type": "array",
      "items": {
        "type": "string"
      },
      "uniqueItems": true
    },
    "enumExample": {
      "description": "This is an example of enum",
      "enum": [
        "one",
        "two",
        "three"
      ]
    },
    "refExample": {
      "description": "This is an example of $ref",
      "$ref": "#/definitions/languageStrings",
      "examples": [
        {
          "en": "English"
        }
      ]
    },
    "dependenciesExample": {
      "description": "This is an example of object, dependencies and required",
      "type": "object",
      // 前述の通り、トップの階層で設定していても、ここでも "additionalProperties": false としなければ、
      // 意図しないフィールドが設定できてしまいます。
      "additionalProperties": false,
      // この定義により、xx を定義した時に、必ず yy を定義する、というルールを付けることが出来ます。
      // ここの例では width、height、depth のいずれかが定義された場合は、unit を定義しなければエラーになります。
      "dependencies": { "width": ["unit"], "height": ["unit"], "depth": ["unit"] },
      // ここに定義されたものは必須項目となります。
      "required": [
        "note"
      ],
      "properties": {
        "width": {
          "type": "number"
        },
        "height": {
          "type": "number"
        },
        "depth": {
          "type": "number"
        },
        "unit": {
          "enum": [
            "mm",
            "cm",
            "m"
          ]
        },
        "description": {
          "$ref": "#/definitions/languageStrings"
        },
        "note": {
          "$ref": "#/definitions/languageStrings"
        }
      }
    },
  },
  // ここに定義されたものは必須項目となります。
  "required": [
    "numberExample",
    "arrayExample"
  ],
  "minProperties": 0
}

バリデーションルールについて

上記以外にも様々なバリデーションルールが設定可能です。
参照先:https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md

セキュリティ対応

https://github.com/ajv-validator/ajv#security-considerations にリストアップされている内容に準拠しているが、以下のように Jest などで確認しておきましょう。

import Ajv from 'ajv'
import schema from 'somewhere/schema.json'

// see: https://github.com/ajv-validator/ajv#security-considerations
describe('isSchemaSecure', () => {
  const ajv = new Ajv()
  const isSchemaSecure = ajv.compile(
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    require('ajv/lib/refs/json-schema-secure.json')
  )
  expect(isSchemaSecure(schema)).toBe(true)
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mongooseを使ってMongoDBのドキュメントを検索するときに、取得するフィールドを指定したい

はじめに

今までMySQLは使ったことがあったのですが、初めてMondoDBを使って、フィールド(RDBでいうカラム)を指定する方法がわからなかったので、まとめます。

やりたいこと

以下のようなドキュメント(RDBでいうテーブル)があったとします。

id name age birthday address
1 Jack 30 19900812 A City
2 Amy 31 19890812 B Town
3 David 32 19880812 C Street

このドキュメントから、idnameだけ取得したいです。

RBDなら、以下のSELECT文で取得できるでしょう。

SELECT (id, name) from USERS;

やり方

mongooseだと、以下の様に取得するフィールドを指定できます。

UsersModel.find({},{ id: 1, name: 1 });

find()の第2引数のオブジェクトの中に、フィールド名: 1と書くと取得対象になり、
フィールド名: 0と書くと、取得対象になります。(デフォルトは1みたいです)。

私がやったときは、上記のように書くと_idが必ずついて来てしまった(MongoDBの仕様?)ので、以下のように書いていました。

UsersModel.find({},{ _id: 0, id: 1, name: 1 });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js】Expoプッシュ通知を一斉送信するWebAPI|配列を任意の数に分割をする

はじめに

現在Expo ReactNativeを使ってネイティブアプリ開発をしています。
Expoを使ってプッシュ通知を送るためにプッシュ通知のTokenを1つ、1つPOSTリクエストを飛ばしてもいいのですが、Expoの仕様では最大100個のTokenをまとめてPOSTできる。そこでN個ずつに配列を分割して二次元配列にして処理する方法を紹介します。

以下のような長さ75の配列があるとする、それを10分割して余った分は10個未満になっても分割をする。
今回は長さ75の配列を10個づつ分割して二次元配列にする。

処理前

スクリーンショット 2020-08-12 午後6.59.47.png

処理後

スクリーンショット 2020-08-12 午後7.02.02.png

コード

const main = () =>{
  //TOKENにみたて複数の数字を配列にセット
  let arr = []
  for (let i = 0; i < 75; i++) {
    arr.push(i)
  }

  console.log(arr)

  let result_arr = []

  //分割数
  const division_count = 10

  //配列の数を分割数で割った値を切り上げた値
  const add_arr_count = Math.ceil(arr.length / division_count)


  //add_arr_countの分だけ空配列を追加する
  for (let i = 0; i < add_arr_count; i++) {
    result_arr.push([])
  }


  //いい感じに分割して配列にいれる
  arr.forEach((item,index)=>{
    result_arr[Math.floor(index/division_count)].push(item)
  })

  console.log(result_arr)
}


(async () => {
  main()
})()

Expoを使って実際に通知を送る

私の環境ではNode.jsを使ってWebAPIを作り、まとめて送信できるようにしています。
また、Token一覧を管理しているのはMySQLを使っています。

Request body
プッシュ通知のタイトルを設定するpush_titleとプッシュ通知の本文部分にあたるpush_body載せてください

※本物のコードは外部から実行できないようにセキュリティ対策をしています

コード

const express = require('express') // expressモジュールを読み込む
const cors = require('cors') //クロスドメインでアクセスを許可する系のやつ
const bodyParser = require('body-parser') //いい感じにGET POSTを解釈するやつ
const util = require('util') // SQL Async/Await
const axios = require('axios')//npm install axios
const mysql = require('mysql')
const multer = require('multer') // multerモジュールを読み込む これがないとBODYの中身をうまく読み取らない

//↑ 各種、「npm install」してください
//↑ 各種、「npm install」してください
//↑ 各種、「npm install」してください
// 例1 npm i express
// 例2 npm i cors

const app = express() // expressアプリを生成する

app.use(bodyParser())
app.use(express.static('web')) // webフォルダの中身を公開する
app.use(cors())//CROS許可
app.use(multer().none()) // multerでブラウザから送信されたデータを解釈する

//サーバの受付ポート番号、SQL接続情報、設定ファイル読み込み
const config = require('./server_config.json')

// config.server.portのポートでサーバを立てる
app.listen(config.server.port, () => console.log('Listening on port ' + config.server.port))

//MySQL接続情報Async/Await★★★★★★★★★★★★★★★★★★★★★★★★★★★★
//★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
const pool_config = {
  host: config.sql.host,
  user: config.sql.user,
  password: config.sql.password,
  port: config.sql.port,
  database: config.sql.database,
  timezone: config.sql.timezone //timezoneの指定省略の場合はシステムローカルになる
}
//★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
//★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

//汎用的にMySQLを発行する
const runQuery = async (sql, data) => {
  console.log('runQuery...')
  console.log('sql: ' + sql)
  console.log('data: ' + data)

  const pool = mysql.createPool(pool_config)
  pool.query = util.promisify(pool.query)//これないと動かない

  const run_sql = mysql.format(sql, data)
  console.log('発行されたquery:\n' + run_sql + '\n')

  const result = await pool.query(run_sql)
  // console.log(JSON.stringify(result))
  pool.end() // mysqlのコネクションのプロセスを終了させる。
  return result
}

const uuid = require('node-uuid')

app.post('/api/v1/expoPushNotice', (req, res, next) => {
  (async () => {
    console.log('/api/v1/expoPushNotice...')

    //私の環境ではMySQLでpush_tokenというTableを作ってそこでTokenを保存しています
    let push_token_list = await runQuery(
      'select `push_token` from `push_token`',
      []
    )

    //通知を送るtoken
    let to_tokens = []
    //分割数
    const division_count = 100
    //配列の追加数 切り上げた値の数だけ配列を追加
    const add_arr_count = Math.ceil(push_token_list.length / division_count)

    console.log('add_arr_countの文だけから配列を追加する')
    //add_arr_countの文だけから配列を追加する
    for (let i = 0; i < add_arr_count; i++) {
      to_tokens.push([])
    }

    push_token_list.forEach((item, index) => {
      to_tokens[Math.floor(index/division_count)].push(item.push_token)
    })

    console.log('to_tokens: ')
    console.log(to_tokens)

    //APIコール
    console.log('pushSend...')
    const req_url = 'https://exp.host/--/api/v2/push/send'

    const headers = {
      headers: {
        Accept: 'application/json',
        'Accept-encoding': 'gzip, deflate',
        'Content-Type': 'application/json',
      },
    }
    //分割した分で別々にPOSTリクエストを飛ばす
    for (let i = 0; i < to_tokens.length; i++) {

      //bodyに好きなデータを載せると、アプリ側でいろいろできます。
      const result = await axios.post(
        req_url,
        {
          'to': to_tokens[i],
          'sound': 'default',
          'title': req.body.push_title,
          'body': req.body.push_body,
          'data': {

          },
        },
        headers
      )

      console.log('result: ')
      console.log(result)
    }


    res.json({
      status: 200, //
      message: 'リクエストは正常に受信されました',
    })

  })(res, next).catch((e) => {
    console.log('サーバエラー : ' + e)
    console.log('res : ' + res)
    console.log('e : ' + e)

    res.json({
      status: 500,
      message: 'サーバエラー\n' + next,
    })
  })
})

Tokenを保存しているDB情報

CREATE TABLE `push_token` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `push_token` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `push_token` (`push_token`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

サーバの設定ファイルを記述しているJSONファイル

{
  "sql": {
    "host": "localhost",
    "user": "hoge",
    "password": "hoge",
    "port": 8889,
    "database": "fuga",
    "timezone": "jst"
  },
  "server":{
    "port": 3003
  }
}

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

自分のためのjsメモ

自分のためのjsメモ

はじめに

忘れないように、また忘れたとき用のメモです。
ちょっとずつ追加していきます

URLのget情報取得

script.js
//http://localhost/index.php?text=hogehoge&page=18
let params = (new URL(document.location)).searchParams;
let text = params.get('text');
let page = parseInt(params.get('page'));
console.log(text);
//hogehoge
console.log(page);
//18

参考→MDN

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

便利だと持ったjsメモ

便利だと持ったjsメモ

はじめに

更新日:2020/08/12
忘れないように、また忘れたとき用のメモです。
ちょっとずつ追加していきます

修正前:自分のためのjsメモ
修正後:便利だと持ったjsメモ
コメントで指摘があったので修正しました。

URLのget情報取得

script.js
//http://localhost/index.php?text=hogehoge&page=18
let params = (new URL(document.location)).searchParams;
let text = params.get('text');
let page = parseInt(params.get('page'));
console.log(text);
//hogehoge
console.log(page);
//18

参考→MDN

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

javaScriptでURLのget情報を簡単に取得したい

はじめに

忘れないように、また忘れたとき用のメモです。
ちょっとずつ追加していきます

修正前:自分のためのjsメモ
修正後:javaScriptでURLのget情報を簡単に取得したい
コメントで指摘があったので修正しました。
更新日:2020/08/12

javaScriptでURLのget情報を簡単に取得したい

script.js
//http://localhost/index.php?text=hogehoge&page=18
let params = (new URL(document.location)).searchParams;
let text = params.get('text');
let page = parseInt(params.get('page'));
console.log(text);
//hogehoge
console.log(page);
//18

参考→MDN

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

ReactでCanvasを使う

ReactでCanvas APIを使う場合どういうタイミングで初期化したらいいかとか
どう状態を持ったらいいか慣れないと結構悩みどころなのでざっくりシンプルなサンプルで実装

app.js
import React,{useState,useEffect} from "react"
import ReactDOM from "react-dom"

const App = () => {
    // contextを状態として持つ
    const [context,setContext] = useState(null)
    // 画像読み込み完了トリガー
    const [loaded,setLoaded] = useState(false)
    // コンポーネントの初期化完了後コンポーネント状態にコンテキストを登録
    useEffect(()=>{
        const canvas = document.getElementById("canvas")
        const canvasContext = canvas.getContext("2d")
        setContext(canvasContext)
    },[])
    // 状態にコンテキストが登録されたらそれに対して操作できる
    useEffect(()=>{
        if(context!==null)
        {
            const img = new Image()
            img.src = "img.jpg" // 描画する画像など
            img.onload = () => {
                context.drawImage(img,0,0)
                // 更にこれに続いて何か処理をしたい場合
                setLoaded(true)
            }
        }
    },[context])
    useEffect(()=>{
        if(loaded)
        {
            // それに続く処理
        }
    },[loaded])
    return <canvas width="1280" height="720" id="canvas"></canvas>
}
ReactDOM.render(<App />, document.getElementById('root'))

のようにHooksのトリガーで順番に制御していけばタイミング問題でそれほど悩まずにReactでもCanvasが使えるはず

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

E2Eテストの始め方 TestCafe② -セレクタについて-

E2Eテストの始め方 TestCafe① -導入編-

エレメントに関する情報を取得するにはSelectorを使用します。
ページ要素を識別し、それらに対してアクション(クリックやドラッグなど)を実行するか、アサーションでその状態を確認することができます。

Selector( init [, options] )
init:選択するDOMノードを識別します。(型:関数、文字列、他のセレクタ、スナップショット、プロミス)
options:オプションを設定します。 (型:オブジェクト)

セレクタの基本的な指定方法は主に以下のようなものがあります。

【セレクタから要素をフィルタリングするメソッド】

メソッド    説明
nth(index) インデックスで要素を検索。index番目
withText コンテンツに指定されたテキストが含まれる要素を検索
正規表現の場合、正規表現に一致するもの
withExactText 指定されたテキスト(完全一致)を持つ要素を検索
withAttribute 指定された属性または属性値を持つ要素を検索
属性名も値も正規表現が使える
filterVisible 表示されている要素を選択
[display:none]や[visibility:hidden]、幅や高さが0のものは対象外
filterHidden 非表示の要素を選択。filterVisible()の反対
filter 指定されたCSSセレクターまたは述語に一致する要素を検索

【選択した要素に関連するDOM要素を検索するメソッド】

メソッド    説明
find 子要素
parent 親要素
child 直下
sibling 兄弟要素(同一階層)
nextSibling 自身以降の兄弟要素
prevSibling 自身以前の兄弟要素

【要素が存在するかどうかを確認するプロパティ】
要素が一致するか、一致する要素の数を確認することができる

プロパティ    タイプ    説明   
exists Boolean 少なくとも1つの一致する要素が存在する場合(true)
count Number 一致する要素の数
childElementCount Number 子要素の数
childNodeCount Number 子ノードの数
hasChildElements Boolean このノードに子HTML要素がある場合(true)
hasChildNodes Boolean このノードに子ノードがある場合(true)
nodeType Number ノードのタイプ
textContent String ノードとその子孫のテキストコンテンツ
メソッド    タイプ    説明   
hasClass(className) Boolean 指定されたクラス名を持つ場合(true)

【指定した要素ノード固有の情報】

プロパティ    タイプ    説明            
attributes Object 要素の属性{name: value,...}形式で返す。
getAttributeメソッドを使用して、属性値にアクセスすることもできます。
boundingClientRect Object 要素のサイズとビューポートに対するその位置。leftrightbottomtopwidthheightが取得できます。getBoundingClientRectPropertyメソッドを使用しこれらのプロパティにアクセスすることも可能。
checked Boolean チェックボックスおよびラジオボタンの場合、現在の状態を取得。他の要素についてはundefindになります。
classNames Array of String 要素のクラスリストを取得
clientHeight Number 要素の内側の高さを取得。paddingは含むが、水平スクロールバーの高さ、境界線、border、marginは含まれない。
clientWidth Number 要素の内側の幅を取得。paddingは含むが、垂直スクロールバーの幅、境界線、border、marginは含まれない。
focused Boolean 要素がフォーカスされている場合true
id String 要素のidを取得
innerText String 「レンダリングされた」要素のテキストコンテンツを取得。
offsetHeight Number 垂直方向のパディングと境界線を含む要素の高さを取得。
offsetWidth Number 垂直パディングとボーダーを含む要素の幅を取得。
selected Boolean <option>要素で現在選択されているものがあるか。
selectedIndex Number <option>要素で現在選択されているもの要素のインデックス。
scrollHeight Number オーバーフローのために画面に表示されないコンテンツを含む、要素のコンテンツの高さを取得。
scrollWidth Number 要素のコンテンツのピクセル単位の幅または要素自体の幅のいずれか大きい方を取得。
scrollTop Number 要素のコンテンツが上にスクロールされるピクセル数を取得。
scrollLeft Number 要素のコンテンツが左にスクロールされるピクセル数を取得。
style Object 要素に適用されているCSSプロパティ:値を取得。
getStylePropertyメソッドを使用してCSSプロパティにアクセスすることもできます。
tagName String 要素のタグ名
value String input要素のvalueを取得。
visible Boolean 要素のvisible状態を取得。display:nonevisibility:hiddenではなく、幅や高さも0でない場合(true)、それ以外はfalse
メソッド    タイプ    説明            
getStyleProperty(propertyName) Object propertyNameのCSSプロパティを取得。styleプロパティを使用して、CSSプロパティのハッシュテーブルにアクセスすることもできます。 
getAttribute(attributeName) String 「attributeName」属性の値を取得。attributesプロパティを使用して、属性のハッシュテーブルにアクセスすることもできます。 
hasAttribute(attributeName) Boolean 「attributeName」の属性が設定されているかどうか 

まずは公式を見てもらうのが一番ですが、さっと確認できるといいな〜と思いまとめてみました!
次回はキャプチャの撮り方について書いていきたいと思います。

公式:DOMNodeStateオブジェクトについて

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

AjaxでGASと通信した時に認証エラーが出る場合の対策

概要

AjaxでGASと通信した際にスクリプトの実行に対して権限を許可していないと認証エラーとなってしまいます。
(スプレッドシートの書き込みやカレンダーの取得等)
対策としてAjaxに失敗した場合に限り、GASでレンダリングしたHTMLを表示させることにしました。

※非同期処理の実装はこちらの記事を参考にさせていただきました。
WebサイトのJSからAjaxでGASの関数を最短で叩く

コード

まずはクライアント側です。ボタンを押したらメールアドレスやログインIDをアラート表示させる実装となっています。
また、ajaxの失敗時にも ?mode=authPage のパラメータ付きで別タブでWEBページを開くようにしています。
URLはajaxと同様にGASのエンドポイントを利用します。

クライアント側のサンプル.html
<!DOCTYPE html>
<html>
<head>
    <title>GASサンプル</title>
    <script src="https://code.jquery.com/jquery-3.3.1.js"></script>
</head>
<body>
    <button onclick="getMailAddress();">メールアドレスを取得</button>
    <button onclick="getUserName();">ユーザー名を取得</button>
    <script>
        function getMailAddress(){
            callAppsScript("mailAddress", function(data) {
                window.alert(data['rs']);
            });
        }
        function getUserName(){
            callAppsScript("userName", function(data) {
                window.alert(data['rs']);
            });
        }
        $(document).ready(function($) {
            // ajaxの処理
            window.callAppsScript = function(mode, callbackFunc) {
                const GAS_URL = "https://example.com/xxxxxxxxxxxxxxxxxxxxxx";
                let request = {'mode': mode};
                $.ajax({
                    type:"get",
                    url: GAS_URL,
                    data: request,
                    dataType: "jsonp",
                    success: function(data) {
                        callbackFunc(data);
                    }
                }).fail(function(jqXHR, textStatus, errorThrown ) {
                    console.log("XMLHttpRequest : " + jqXHR.status + "\n" + "textStatus : " + textStatus + "\n" + "errorThrown : " + errorThrown.message);
                    if(confirm('通信に失敗しました。認証ページを開きますか?')){
                        window.open(GAS_URL+'?mode=authPage', '_blank');
                    }
                });
            }
        });
    </script>
</body>
</html>

続いてGAS側です。パラメータに応じてメールアドレスやログインIDを返していますが、
mode=authPage の場合のみ index.html をレンダリングさせています。

main.js
function doGet(e) {
  let params = e.parameter;
  let res = {};
  switch(params.mode) {
    case 'mailAddress':
      res['rs'] = Session.getActiveUser().getEmail();
      break;
    case 'userName':
      res['rs'] = Session.getActiveUser().getUserLoginId();
      break;
    case 'authPage':
      let htmlOutput = HtmlService.createTemplateFromFile('index.html').evaluate();
      htmlOutput.setTitle('GAS認証ページ').addMetaTag('viewport','width=device-width, initial-scale=1')
      return htmlOutput;
    default:
      res['rs'] = 'パラメータが定義されていません。';
      break;
  }
  let callback = params.callback;
  let output = ContentService.createTextOutput();
  let responseText;
  if (callback) {
    responseText = callback + "(" + JSON.stringify(res) + ")";
    output.setMimeType(ContentService.MimeType.JAVASCRIPT);
  } else {
    responseText = JSON.stringify(res);
    output.setMimeType(ContentService.MimeType.JSON);
  }
  output.setContent(responseText);
  return output;
}

描画するのは簡易的なHTMLです。
GASのエディタページで ファイル->NEW->HTML と操作すると作成できます。

index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <span>認証が完了しました。このタブを閉じてアプリケーションに戻ってください。</span>
  </body>
</html>

ここまで出来たら 公開 -> WEBアプリケーションとして導入 から公開範囲を設定します。
例:
Execute the app as: User accessing the web app
Who has access to the app: 全ユーザー
gas.png

動作

実際の挙動です。紙芝居でご覧ください。

クライアント側HTMLのボタン押下でconfirmが表示されます。
confirm.png

GAS側のHTMLにアクセスする際にいつもの認証ダイアログが表示されます。
「許可」を押して進みます。
kengen.png

GAS側のHTMLが表示されます。
fin.png

元のページに戻って再度ボタンを押すと正常に機能します。
confirm - コピー.png

備考

簡単な実装ですが、問題の解決策としては十分かと思います。
switch文の中で他と違う処理を実行しているのが気持ち悪いのでパラメータ自体を別に用意した方がいいかもしれません。

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

【JS】GASさん初めまして

基本的によく使う実装たち

// スプレッドシートの初期化
const sheet = SpreadsheetApp.getActiveSheet();

// 入力されている最後の列
const lastColumn = sheet.getLastColumn();

// A列に aaa という文字列を挿入する
// 1, 1 は、行 列の順で記載できる
sheet.getRange(1, 1).setValue("aaa");

// B列の最後に入力されている行を取得
const lastRow = sheet.getRange(1, 2).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();

【GAS】1行で書ける!特定列の最終行・特定行の最終列を取得する方法|もりさんのプログラミング手帳

メール検索

const query = "subject:新着の応募者がいます";
// 0から100件まで取得する。
const myThreads = GmailApp.search(query, 0, 100);
// スレッドからメールを取得する
const myMsgs = GmailApp.getMessagesForThreads(myThreads);

for (let threadIndex = 0 ; threadIndex < myThreads.length ; threadIndex++) {
  const mailBody = myMsgs[threadIndex][0].getPlainBody();
  const date = myMsgs[threadIndex][0].getDate();
  // 抽出した情報は A2 セルに追加して書き込んでいく。
  sheet.getRange(threadIndex + 2, lastColumn - 1).setValue(mailBody);
  sheet.getRange(threadIndex + 2, lastColumn).setValue(date);
}       

【GAS】GmailApp.searchを使ってGmailをさまざまな条件で取得する使い方 | エイトベース
【GAS】新たな問い合わせメールをGmailで取得しスプレッドシートに随時追加する

メール送信

const emailAddress = "test@test.com"
const subject = "挨拶だよ"
const message = "こんにちは"
MailApp.sendEmail(emailAddress, subject, message);

GASのスケジューラー

GAS 単体でスケジューラを実装することもできます。
分、時間、日付、週、月単位で指定することができます。
image.png
image.png

GASのデバッグ

ブレイクポイント

GAS上でブレイクポイントを指定して、変数の中身を確認することができます。
image.png

ログ

表示→ログ(コマンド + Enter)でログを確認することができます。
image.png

ローカルで実装

ローカルで実装できるとは。今度試してみよう。
GAS ビギナーが GAS を使いこなすために知るべきこと 10 選 - Qiita

Cloud Functions と組み合わせる

Google Cloud Functions と Puppeteer で動的ウェブページを実行してコンテンツを返す API を作る - Qiita
Puppeteer + GCP Functionsでサーバレスなスクレイピング - Qiita
Cloud Functions with Puppeteer + Google Apps Script でスクレイピングサーバーをサクッと作る - Qiita

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

TypeScript + Node.jsプロジェクトにESLint + Prettierを導入する手順2020

TL;DR

https://github.com/notakaos/typescript-node-base-with-eslint-prettier

完成形のソースコードはこちら↑

この記事の趣旨

TypeScript + Node.js プロジェクトのはじめかた2020 で作成したTypeScript + Node.jsのプロジェクトに ESLint / Pretiter / husky & lint-staged を導入する手順を紹介します。

今回導入するツールとバージョンは以下になります。

項目 バージョン
ESLint 7.6.0
Prettier 2.0.5
husky 4.2.5
lint-staged 10.2.11

動作環境

node と npm はインストール済みとします。

$ node -v
v12.18.3
$ npm -v
6.14.6

また、今回の記事はmacOSにて検証しております。

$ uname -v
Darwin Kernel Version 19.6.0: Sun Jul  5 00:43:10 PDT 2020; root:xnu-6153.141.1~9/RELEASE_X86_64

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G73

Linux環境だと手順はほぼ同じだと思いますが、Windows環境の場合は読み替えが必要になるかもしれません。

ESLint

https://eslint.org/

ESLintについて

ESLintイーエスリントはJavaScriptコードのエラーチェックを行うLinterと呼ばれるツールの一つです。

JavaScriptは動的な言語なので、実行するまでエラーがあるのかどうかわからない部分があります。
ESLintを使うことで、JavaScriptのコードを実行することなくコード上の問題を発見するのに役立ちます。

また、ESLintは本来JavaScript用ですが、TypeScript用のプラグインを追加することにより、TypeScriptでもESLintが使えるようになります。

以下ではTypeScript (+ Node.js)なプロジェクトへのESLintの導入手順を紹介します。

ESLint (+ TypeScriptプラグイン)の導入

https://github.com/notakaos/typescript-node-base にESLintを導入します。

# typescript-node-base リポジトリをローカルで作成済みとします 
cd typescript-node-base

Node.jsプロジェクトにESLintとESLintのTypeScript用プラグインを追加します。

typescript-node-base
npm install -D eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin

# ESLintのバージョン確認
./node_modules/.bin/eslint --version
#=> v7.6.0

次に、ESLint用のTypeScript設定ファイル tsconfig.eslint.json を作成します。

typescript-node-base
# tsconfig.eslint.json ファイルの作成
touch tsconfig.eslint.json

# エディターで編集(お好みのエディターをお使いください)
vim tsconfig.eslint.json
tsconfig.eslint.json
{
  "extends": "./tsconfig.json",
  "include": [
    "src/**/*.ts",
    ".eslintrc.js"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

そして、ESLint用の設定ファイル .eslintrc.js をプロジェクトディレクトリ直下に作成します。

typescript-node-base
# .eslintrc.js ファイルの作成
touch .eslintrc.js

# エディターで編集(お好みのエディターをお使いください)
vim .eslintrc.js
~/typescript-node-base/.eslintrc.js
module.exports = {
  root: true,
  env: {
    es6: true,
    node: true,
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module',
    ecmaVersion: 2019, // Node.js 12の場合は2019、他のバージョンのNode.jsを利用している場合は場合は適宜変更する
    tsconfigRootDir: __dirname,
    project: ['./tsconfig.eslint.json']
  },
  plugins: [
    '@typescript-eslint',
  ],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
  ],
  rules: {
  },
};

なお、extendsでいくつかの項目がありますが、extendsはあらかじめ用意されたESLintルールのセットになります。これを入れることによって、以下のルールを適用しています。

これで、ESLint導入完了です。

ESLintの実行

早速、ESLintを実行してみましょう。

typescript-node-base
npx eslint src/index.ts
実行結果

何も表示されなければ成功です!

設定やソースコードに問題がない場合、何も表示されないのが正常です。
もしここでエラーが表示される場合は記述ミスが考えられますので、各種設定ファイルやソースコードを見直してください。

次に、ESLintでエラーを検知できるか確認してみましょう。
src/index.ts を以下のように編集します。

src/index.ts
function hello(name: string): string {
  return `Hello, ${name}!`;
}

- console.log(hello('TypeScript'));
+ console.log(hello('TypeScript')

最後の行の閉じカッコを一つ削除して、文法エラーを意図的に発生させます。
ソースコードを書き換えたら保存し、ESLintを実行します。

typescript-node-base
npx eslint src/index.ts

すると以下のようなエラーが発生します。

実行結果
/Users/notakaos/typescript-node-base/src/index.ts
  6:0  error  Parsing error: ')' expected

✖ 1 problem (1 error, 0 warnings)

「パースエラー: ) が期待されている(が存在しない)」というエラーがでましたね。
ESLintで文法チェックが正しく動いていることがわかります。

ちなみに、文法のチェックに関してはtscでも同様のチェックができます。
(--noEmitオプションをつけてjsファイルを生成しないようにします)

typescript-node-base
npx tsc --noEmit
実行結果
src/index.ts:6:1 - error TS1005: ')' expected.

6 



Found 1 error.

そのため、文法チェックだけだとtscのみでよいのでは? と思われるかもしれませんが、ESLintにはtscに含まれていないエラーチェック機能が入っているため、tscとESLintを併用することによりエラーの検知率をあげることができます。

ここで、tscではエラーにならず、ESLintでエラーになる例を見てみましょう。
src/index.ts に以下の記述を追加します。

src/index.ts
function hello(name: string): string {
  return `Hello, ${name}!`;
}

- console.log(hello("TypeScript")
+ console.log(hello("TypeScript")); // 元に戻す


+ function example() {} // どこからも呼ばれない中身が空の関数を用意

まずはtscを実行してみます。

npx tsc --noEmit
実行結果

特にエラーもなく正常にコマンドが終了しました。
次にESLintを実行します。

npx eslint src/index.ts
eslint実行結果
/Users/notakaos/typescript-node-base/src/index.ts
  7:10  warning  'example' is defined but never used  @typescript-eslint/no-unused-vars
  7:20  error    Unexpected empty function 'example'  @typescript-eslint/no-empty-function

✖ 2 problems (1 error, 1 warning)

ESLintでは1つの警告と1つのエラーが発生しましたね。

警告・エラーの内容について、

  • 「警告: example関数は定義されているものの使われていない」
  • 「エラー: 空の関数は期待されていない」

といった内容になっています。

このように、tscでは通るもののESLintではエラーになるようなものもあり、
両者を適切に設定することでよりバグが少ないコードを書くことができるようになります。

最後に毎回 eslint / tsc のコマンドを手打ちすると面倒なので、簡単にエラーチェックができるようにpackage.jsonのscriptsに設定を追加します。

package.json
{
  ...
  "scripts": {
    "dev": "ts-node src/index.ts",
    "dev:watch": "ts-node-dev --respawn src/index.ts",
    "clean": "rimraf dist/*",
    "tsc": "tsc",
    "build": "npm-run-all clean tsc",
-   "start": "node ."
+   "start": "node .",
+   "check-types": "tsc --noEmit",
+   "eslint": "eslint src/**/*.ts",
+   "eslint:fix": "eslint src/**/*.ts --fix",
+   "lint": "npm-run-all eslint check-types"
  },
  ...
}

エラーチェックを実行する時は以下のコマンドを実行します。

typescript-node-base
npm run lint

こうすることで、一度のコマンド実行で eslinttsc の両方を実行することができます。

また、以下のように個別に実行することもできます。

typescript-node-base
# tsc --noEmit だけ実行
npm run check-types 

# eslint だけ実行
npm run eslint

# eslint のエラーを(できるものだけ)自動修復
npm run eslint:fix

普段は npm run lint を使い、必要に応じて個別のコマンドを使い分けることになります。

Prettier

https://prettier.io/

Prettierについて

Prettierプリティアはソースコードの整形ツール(コードフォーマッター)の一つです。
JavaScript/TypeScript/JSON等の形式に対応しています。

Prettierを使うことで、スペースやインデント、文字列のクオートの統一、1行が長くなりすぎた場合の改行位置調整などを自動で行ってくれます。

Prettier等のコードフォーマッターを使わない場合、実装者によってコードの書き方がばらばらになり、コードレビュー時にインデント位置やスペース等の指摘が入ってしまうことがあります。それだと本質的なディスカッションができません。そのため、フォーマッターを設定しておくことで、コードの書き方が統一され、ロジックのレビューに集中できるようになる効果があります。

ちなみに、ESLintにもコードフォーマット機能があるため、ESLintと併せてPrettierを利用する場合はESLint側のフォーマットルールをOFFにする必要があります。幸い、PrettierとESLintのフォーマットルールがぶつからないようにするためのルールセット(config)が用意されていますので、そちらも併せて導入します。

Prettier (+ eslint-config-prettier) の導入

PrettierとESLintのPrettier configをインストールします。

typescript-node-base
# --save-exact でバージョンを固定してdevインストール
npm install -D --save-exact prettier eslint-config-prettier

# Prettierのバージョン確認
./node_modules/.bin/prettier --version
#=> 2.0.5

次に、Prettier用の設定ファイルをプロジェクトディレクトリ直下に作成します。

typescript-node-base
echo "{}"> .prettierrc.json
.prettierrc.json
{}

.prettierrc.json は必要に応じて書き換えてください

また、prettierの対象外となるファイルを指定する .prettierignore をプロジェクトディレクトリ直下に作成します。

typescript-node-base
touch .prettierignore

# エディターで編集
vim .prettierignore
.prettierignore
# Ignore artifacts:
/dist
node_modules
package.json
package-lock.json
tsconfig.json
tsconfig.eslint.json

このままだと、ESLintのコードフォーマット機能とPrettierのコードフォーマット機能がコンフリクトしてしまうので、.eslintrc.jsのextendsに prettierprettier/@typescript-eslint の記述を追加します(追加する位置はextendsの最後の方になります)。

.eslintrc.js
module.exports = {
  // ...
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
+   'prettier',
+   'prettier/@typescript-eslint',
  ],
  // ...
}

これでPrettierの設定は完了です。

コラム: eslint-plugin-prettier について

インターネット上でESlintとPrettierの導入方法を調べると、 eslint-plugin-prettier も一緒に入れる方法が紹介されていることがあります。

いつからかはわかりませんが、 公式ドキュメントによると、eslint-plugin-prettier が 一般的には推奨されなくなっている ようです。

なぜ非推奨なのかというと、以下のような理由が挙げられています。

  • エディターに赤い波線がたくさん表示され、煩わしくなる
  • Prettierを直接実行するよりも遅くなる
  • 物事が壊れる可能性がある間接層の1つになる

そのため、この記事では eslint-plugin-prettier を導入せず、eslintとは別にpretiterを実行する形を採用しております。

Prettierの実行

では、Prettierを実行してみましょう。

まず src/index.ts を以下のようにインデントなどがバラバラな状態に書き換えます。

src/index.ts
function
hello(name:string):string{
return `Hello, ${name}!`
}

console.log(hello("TypeScript"
))

そして、以下のコマンドでPrettierを実行します。

typescript-node-base
npx prettier --write src/index.ts

すると、以下のようにコードが修正されます。

src/index.ts
function hello(name: string): string {
  return `Hello, ${name}!`;
}

console.log(hello("TypeScript"));

きれいに整形されましたね。

Prettierのnpm-scripts設定

あとは、prettierを簡単に実行できるようにpackage.jsonのscriptsに追記します。

Prettier単体で実行する format と、 eslint / tsc / prettier を1度に実行する lint:fix を追加しています。

package.json
{
  ...
  "scripts": {
    "dev": "ts-node src/index.ts",
    "dev:watch": "ts-node-dev --respawn src/index.ts",
    "clean": "rimraf dist/*",
    "tsc": "tsc",
    "build": "npm-run-all clean tsc",
    "start": "node .",
    "check-types": "tsc --noEmit",
    "eslint": "eslint src/**/*.ts",
    "eslint:fix": "eslint src/**/*.ts --fix",
+   "format": "prettier --write 'src/**/*.{js,ts,json}'",
-   "lint": "npm-run-all eslint check-types"
+   "lint": "npm-run-all eslint check-types",
+   "lint:fix": "npm-run-all eslint:fix check-types format"
  },
  ...
}

Prettierのみを実行する場合は以下のコマンドを実行します。

typescript-node-base
npm run format

eslinttscも併せて実行する場合は、以下のコマンドを実行します。

typescript-node-base
npm run lint:fix

これでPrettierの導入完了です。

Git commit時に自動でlintを実行する(huskyとlint-staged)

これまでの手順でESLintとPrettierを導入しましたが、コードを修正するたびに lint:fix コマンドを手動で実行する必要がありました。
そのため、lintをかけていないコードを間違ってコミットしてしまうことがあります。

これを防ぐため、git commit時に esLint/ tsc / prettier を自動的に実行するように huskylint-staged を導入します。

huskyとは

https://github.com/typicode/husky

gitのcommit前やpush前などに特定のコマンドを実行するためのGit hooksを簡単に作成するためのツールです。後述のlint-stagedと組み合わせて使います。

lint-stagedとは

https://github.com/okonet/lint-staged

git add でステージングに追加されたファイルに対して、指定したコマンドを実行します。

huskyとlint-stagedの導入

huskyとlint-stagedは以下のコマンドを実行するとpackage.jsonに自動で設定が追加されます。

typescript-node-base
npx mrm lint-staged
package.json
{
  ...
  "devDependencies": {
    "@types/node": "^12.12.54",
    "@typescript-eslint/eslint-plugin": "^3.9.0",
    "@typescript-eslint/parser": "^3.9.0",
    "eslint": "^7.6.0",
    "eslint-config-prettier": "6.11.0",
+   "husky": "^4.2.5",
+   "lint-staged": "^10.2.11",
    "npm-run-all": "^4.1.5",
    "prettier": "2.0.5",
    "rimraf": "^3.0.2",
    "ts-node": "^8.10.2",
    "ts-node-dev": "^1.0.0-pre.56",
    "typescript": "^3.9.7"
- }
+ },
+ "husky": {
+   "hooks": {
+     "pre-commit": "lint-staged"
+   }
+ },
+ "lint-staged": {
+   "*.js": "eslint --cache --fix",
+   "*.{js,ts,json}": "prettier --write"
+ }
}

そのままではTypeScriptファイルにおいて、ESLintとtscが実行されないので、lint-stage の部分を少し修正します。

package.json
{
  ...
  "lint-staged": {
-   "*.js": "eslint --cache --fix",
+   "*.{js,ts}": "eslint --cache --fix",
+   "*.ts": "tsc --noEmit",
    "*.{js,ts,json}": "prettier --write"
  }
}

これで設定完了です。

husky & lint-staged を試す

それではhuskyとlint-stagedを試してみましょう。

まず src/index.ts を以下のように書き換えます。

src/index.ts
function 
hello(name: string): string {
return `Hello, ${name}!`;
}

console.log(hello("TypeScript!!!!!")
);

そしてgit addを行い、その後git commitを実行します。

typescript-node-base
git add src/index.ts
git commit -m "Update src/index.ts"
実行結果
husky > pre-commit (node v12.18.3)
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up... 
[feature/husky-lint-staged f69684a] Update src/index.ts
 1 file changed, 1 insertion(+), 1 deletion(-)

eslint / tsc / prettier の各コマンドが実行されたのがわかります。

そして src/index.ts を確認してみると、prettierによって自動整形されているのがわかります。

typescript-node-base
function hello(name: string): string {
  return `Hello, ${name}!`;
}

console.log(hello("TypeScript!!!!!"));

git log -p で差分を確認してみましょう。

typescript-node-base
git log -p
実行結果
...
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,4 +2,4 @@ function hello(name: string): string {
   return `Hello, ${name}!`;
 }

-console.log(hello("TypeScript"));
+console.log(hello("TypeScript!!!!!"));

prettierでコードフォーマットされた後にcommitされていますね。

これでlintをかけずにcommitされることを防ぐことができます。

設定は以上です。

あとはお好みでカスタマイズしましょう!

成果物

以上の手順を実行した完成形のソースコードは以下のリポジトリをご参照ください。

https://github.com/notakaos/typescript-node-base-with-eslint-prettier

参考

変更履歴

  • 2020/8/12 記事公開
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vuejs + axios + Slack Incoming WebHooks

フロントのJS(axios)からIncoming WebHooksにリクエストするさいのメモ
CORSで怒られてたところ、AuthorizationとContent-Typeヘッダーを削除することで上手くいった。

    var params = {
      'blocks': [{
        'type': 'divider'
      }, {
        'type': 'section',
        'text': {
          'type': 'mrkdwn',
          'text': 'message'
        }
      }]
    }
    async function slack (payload) {
      var WEBHOOK_URL = 'https://hooks.slack.com/services/*******/*******/*********'
      const res = await axios.post(WEBHOOK_URL, JSON.stringify(payload), {
        withCredentials: false,
        transformRequest: [(data, headers) => {
          delete headers.common.Authorization
          delete headers.post['Content-Type']
          return data
        }]
      })
      return res.data
    }

    slack(params).then(console.log)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jestとreact-testing-libraryでreactのテストをする

はじめに

1回目のReactを使用してWeb画面を作成するでは、reactの簡単なサンプルと共に説明をまとめました。
2回目のReduxとReduxToolkitを使用してReact内でデータを管理するでは、reduxのの簡単なサンプルと共に説明をまとめました。
3回目のReactのRedux内でaxiosを使用した通信をするでは、axiosを使用してREST APIから情報を取得するサンプルと説明をまとめました。
今回は、jestとreact-testing-libraryで仮想DOMのテストをしてみます。

環境

  • node.js: v12.18.2
  • webpack: 4.44.1
  • React: 16.13.1
  • Redux: 7.2.1
  • axios: 0.19.2
  • jest: 26.3.0
  • react-testing-library: 10.4.8

環境作成

今までの続きとなります。
環境構築などはそちらを見てください。

jest

テストをするために、その枠組みを提供するjestというライブラリを使用します。

jestのインストール

yarn add --dev babel-jest react-test-renderer
npm install -g jest

react-testing-library

reactが生成する仮想DOMの操作をするためにreact-testing-libraryというライブラリを使用します。

react-testing-libraryのインストール

npm install --save-dev @testing-library/react

axios-mock-adapter

axiosを使用して通信を行っていますが、テストのたびにREST APIのサーバを起動するのは手間なのでモックを使用してaxiosの処理を置き換えます。

axios-mock-adapterのインストール

npm install --save-dev axios-mock-adapter

テストソースを作成する

テストソースを入れるフォルダの作成

テスト用ソースを入れるフォルダとしてルートフォルダの直下にtestフォルダを作成します。

project_root
├─dist   // ビルド後のファイルを格納 
├─public // htmlを格納
├─src    // reactのJavaScriptファイルやCSSファイルを格納
├─test    // reactのテストファイルを格納

テストファイルの作成

jestは×××.spec.jsや×××.test.jsというファイルを読み込んで実行するため、それに沿ったファイル名にしてください。

Message.spec.js
import React from "react";
import { render, cleanup, fireEvent } from '@testing-library/react';
import MockAdapter from "axios-mock-adapter";
import axios from 'axios';

import { Message } from "../src/Message";

import { Provider } from 'react-redux'
import store from '../store/store'

const mockAxios = new MockAdapter(axios);

mockAxios.onGet("http://localhost:3000/mess_api").reply(200, 
    [{ id: 1, message: "mock hello" }],
);
// テスト実行後にDOMをunmount, cleanupします
afterEach(cleanup)

describe("Messageテスト", () => {
    it("初期値:メッセージが表示される", () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // メッセージが2つ(テキストとボタンにあるか)
        expect(messageRender.getAllByText("メッセージ")).toHaveLength(2);
        // こんにちはが1つ(ボタンにあるか)
        expect(messageRender.getAllByText("こんにちは")).toHaveLength(1);
        // 通信が1つ(ボタンにあるか)
        expect(messageRender.getAllByText("通信")).toHaveLength(1);
        // ボタンが2つあるか)
        expect(messageRender.getAllByRole("button")).toHaveLength(3);
    });

    it("メッセージを押すと入力したものが表示される", () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // こんにちはが1つ(ボタンにあるか)
        const inputElement = messageRender.getByRole("textbox");
        inputElement.innerHTML = "テスト";
        fireEvent.change(inputElement);
        // こんにちはが1つ(ボタンにあるか)
        const messageElement = messageRender.getAllByText("メッセージ");
        // 通信が1つ(ボタンにあるか)
        fireEvent.click(messageElement[0]);
        // 通信が1つ(ボタンにあるか)
        expect(messageRender.getAllByText("テスト")).toHaveLength(1);
    });

    test("通信のテスト", async () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // 通信ボタンを取得
        const messageElement = messageRender.getAllByText("通信");
        // 通信ボタンをクリック。失敗すればエラーになる
        await fireEvent.click(messageElement[0]);
    });
});

jestの形式

jestは指定されたフォーマットにしたがって書くことによってテストの実行をしてくれます。

Message.spec.js
インポート

// afterEach内の関数はテスト後に実行される
afterEach(cleanup)

// テストの塊
describe("Messageテスト", () => {
    // テスト
    it("初期値:メッセージが表示される", () => {
        ~~~ テストの内容 ~~~ 
    });
});

jestのフォーマットはこのような形になります。他にもテスト前やテストスイート前後にする処理を登録できます。

仮想DOMの生成

仮想DOMの生成にはreact-testing-libraryのrenderを使用します。この中に、テストしたいアプリのソースを登録します。
今回はMessageコンポーネントのテストをしたいのでrenderに入れていますが、storeも使っているためそれも適用しています。

Message.spec.js
import React from "react";
import { render, cleanup, fireEvent } from '@testing-library/react';

import { Message } from "../src/Message";

import { Provider } from 'react-redux'
import store from '../store/store'

        ~~~ 省略 ~~~ 

        // 仮想DOMの生成
        const messageRender = render(<Provider store={store}><Message /></Provider>);
    });

仮想DOMの操作

生成した仮想DOMに対してgetByXXXやfindByXXXといった関数を実行すると仮想DOM内の要素を抜き出すことができます。要素のイベントを発火させるためにはfireEvent.change(DOMElement);で発火させます。
今回は入力するためにgetByRole("textbox")で抜き出して、値を入れた後にfireEvent.change(inputElement);で反映させています。
さらにボタンを押すためにgetAllByText("メッセージ")で抜き出した後に、メッセージという文字列が2つあり最初の要素がボタンであるため、そっちのイベントを発火させます。

Message.spec.js
        ~~~ 省略 ~~~ 

        // 入力するテキストボックスを取得
        const inputElement = messageRender.getByRole("textbox");
        // テキストの入力と反映。
        inputElement.innerHTML = "テスト";
        fireEvent.change(inputElement);
        // ボタンの取得
        const messageElement = messageRender.getAllByText("メッセージ");
        // ボタンのクリック
        fireEvent.click(messageElement[0]);
        // ボタンを押した結果を確認
        expect(messageRender.getAllByText("テスト")).toHaveLength(1);
        ~~~ 省略 ~~~ 

axiosのモックを生成

モックを使用するときは、MockAdapterを使用して、モックを生成した後に、モック内の処理を記載します。
今回はgetのリクエストを置き換えるのでonGet(URL).reply(httpステータス,レスポンスボディ)を使用することにより、モック内の処理を定義します。

Message.spec.js
        ~~~ 省略 ~~~ 
import MockAdapter from "axios-mock-adapter";
import axios from 'axios';

// mockの生成
const mockAxios = new MockAdapter(axios);

// mock内容を定義
mockAxios.onGet("http://localhost:3000/mess_api").reply(200, 
    [{ id: 1, message: "mock hello" }],
);
        ~~~ 省略 ~~~ 

テストソースの実行

プロジェクトルートでjestコマンドを実行することでテストできます。

jest

終わりに

厳密にテストする場合、action内のテストやstateのテストといった細かい単位でテストすることが可能になります。
それによってテスト結果がNGになった場合に、原因となる場所が限定されるので調査がしやすくなったり、修正後のテストの範囲を限定できたりと様々なメリットがあります。
しかし、そこら辺のテストをちゃんと書くにはReactのことを理解する必要がある上に時間がかかってしまうため、
Reactへのなじみのない人が実施すると時間だけがかかってしまい、テストソースがバグだらけとなったりしてしまいます。
厳密にテストするか大きな枠でテストするかは理解度と時間、テストの影響のトレードオフかなと思っています。

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

[javascript]カウントダウンタイマーを作成してみた。

カウントダウンタイマーをたまに見かけるけど作ったことがなかったので
作ってみました。

コード

html

<div id="log"></div>

javascriptでタイマーを出すので表示させるためのdivを用意しておきます。

javascript

const totalTime = 10000;
const deadTime = Date.now();

const timeId = setInterval(() => {
  const currentTime = Date.now();

  // 差分を求める
  const diff = currentTime - deadTime;

  const diffSec = totalTime - diff;

  //ミリ秒を整数に変換
  const remainSec = Math.ceil(diffSec / 1000);

  let text = `残り${remainSec}秒`;

  // 0秒以下になったら
  if (diffSec <= 0) {
    clearInterval(timeId);

    // タイマー終了の文言を表示する
    text = "終了";
  }

  // 画面に表示する
  document.querySelector('#log').innerHTML = text;
})

少し解説するとまず
totalTimeで10秒のミリ秒を設定しています。
deadTimeには読み込まれたときの時間を取得しています。

const diffで読み込まれた時間とsetintervalの中のconst currentTimeで差分を計算し、
const diffSecでtotalTimeからdiffを引いた値を計算しています。

const remainSecではMath.ceil関数を使用して引数で与えた数を整数に変換しています。

diffSecが0秒以下になったらclearIntervalでsetIntervalを止めています。

まとめ

こんな感じでカウントダウンタイマーを作成できました。
ただ、これだけだと正直あまり使いどころはないので次回は期日を設けてその期日まで後何日かを表示するような使いどころが多そうなカウントダウンタイマーを作ってみたいと思います。

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

JavaScriptの制御構文(2)〜while/do...while命令〜

はじめに

こんにちは。
今日は、昨日に引き続き、JavaScriptの文法の復習をしておりまして、こちらの方でまとめてみます。今回から複数回、繰り返し処理の構文について復習していきます。

繰り返し処理には、
* while
* do…while
* for
* for…in
* for…of
などがあります。
その中でも、今回は、while/do…while命令に焦点を当ててみます。

While命令とdo...while命令

まずは、それぞれの命令の書き方について!
while命令

while (条件式) {
    条件式がtrueである間に繰り返す処理
}

do...while命令

do {
    条件式がtrueである間に繰り返される処理
} while (条件式); 

do...while命令において、最後のwhile(条件式)の後にセミコロンをつけるのを忘れない!

whileの位置が違うだけで、同じような処理に見えます。一体、どこが違うのでしょうか。

while命令とdo...while命令の違い

whileの位置の違いが重要です。
whileの後に続く条件式を「いつ判断するのか」によって、処理結果が変わってきます。

while命令の場合
前置判断。
処理を実行する前に、条件式を判断し、trueであれば、実行する。falseであれば、実行しない。場合によっては、最初からfalseであれば、処理を全く実行しないこともありうる。

do...while命令の場合
後置判断。
最後にwhile(条件式)を配置していることからも、後から、条件式を判断し、falseが出てきたら終わり。falseであろうがなんであろうが、とにかく最低1回は処理を実行する。で、実行してからtrueかfalseか判断する。trueであれば、また処理を継続して、falseであれば、そこでストップする。

実際のコードで比較してみましょう!

while命令ver.

let x = 8;
while (x < 10) {
    console.log(x);
    x++;
}
// 結果
// 8
// 9

do...while命令ver.

let x = 8;
do {
    console.log(x);
    x++; 
} while (x < 10); // すでに1加算されたxと条件式を比較する事になる!

// 結果
// 8
// 9

後置判断でも、同じ挙動になったのは、do…while命令で、x=9になった時のループ処理で、最後にx++でx=10になり、その直後に条件判断をすると、x=10は、条件式x<10を満たさないからfalseになり、処理が終了するからです。

もし仮に、次のような、コードを実行するとどうなるのでしょうか。
while命令ver.

let x = 10;
while (x < 10) {
    console.log(x);
    x++;
}

// 結果
// 何も起こらない

do...while命令ver.

let x = 10;
do {
    console.log(x);
    x++;
} while (x < 10);

// 結果
// 10

上記のように、後置判断である、do…whileは最低でも1度はループ処理を実行するので、xの値が表示されるが、最後に条件式を判定してみると、falseになるので、処理は終了します。

しかし、前置判断である、while命令の場合は、初期値を判断するため、初期値がすでに条件式を満たさない10が来てfalseになったので、処理の実行には至らず、そこで、終了してしまったので、consoleには何も表示されませんでした。

復習していて、
どこで条件が判断されるのかだけに囚われず、どこで何がどう判断されるのか、処理の順番や細部に気を付けることが重要だなと感じました!

個人開発でも、あまり、while/do..while命令を利用することがなかったので、詳しく理解できていませんでしたが、これから何かの機会で利用できたらなと思います!!

理解不足による不備等ございましたら、コメントをお願い致します!

参考文献・資料

山田祥寛著『[改訂新版]JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで』(2018)
※第1刷は、2016年。

do...while命令の挙動の理解がきちんとできていなかったので、teratailにて、質問をさせて頂きました。
https://teratail.com/questions/284149

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

【Node.js 超入門】Node.jsとExpessを使ってCRUD処理できるサーバサイドの実装をする!

【Node.js 超入門】Node.jsとExpessを使ってCRUD処理できるサーバサイドの実装をする!

【Node.js 超入門】という参考書を読んで、Node.jsで実装した内容を備忘として書きます。

今回は、データベースを使って画面からユーザ情報を登録、更新などCRUD処理できるアプリケーションを作成します。
メインとしてNode.jsとExpressを使い、サーバサイドの処理を作ります。
データベースはMySQLを使います。

大まかな流れとしては、
・環境構築
・コード実装
・動作確認

続きはこちらの記事にまとめています!
https://masa-enjoy.com/nodejs-learning1

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

JavaScriptで非同期処理

こんにちは

どうも、僕です。

今日はJavaScriptについてです。別に非同期処理をするならJavaScriptじゃなくてもいいんですけど、割と順番把握がしやすいかなとかいう理由でJavaScriptにしました。ちなみに過去にPythonの並列処理でハマった経験があるので、個人的にPythonでの並列処理や非同期処理は苦手意識があります。

非同期処理とは

そもそも非同期処理とはなんなのかについて話します。

通常、プログラムは上から下に向かって逐次的に処理を進めていきます。上の処理が終了するまで次の処理には基本的には進みません。

const f1 = () => {
    console.log("say1")
}

const f2 = () => {
    console.log("say2")
}

const f3 = () => {
    console.log("say3")
}

f1()
f2()
f3()

例えばこんな感じの素直なプログラムがあるとします。
これの出力は当然のように

say1
say2
say3

となることがわかります。
これは普通の処理です。

次にこれを非同期で処理したらどうなるでしょうか?
コードはこんな感じになります。

const f1 = async () => {
    return new Promise(function(resolve) {
        setTimeout(
            resolve, 
            100, 
            console.log("say1"));
    })
}

const f2 = async () => {
    return new Promise(function(resolve) {
        setTimeout(
            resolve, 
            100, 
            console.log("say2"));
    })
}

const f3 = async () => {
    return new Promise(function(resolve) {
        setTimeout(
            resolve, 
            100, 
            console.log("say3"));
    })
}


f1().then(
    () => {
        return f2()
    }
).then(
    () => { 
        return f3()
     }
).catch(
    (err) => { 
        console.log(err) 
    }
)

これを実行すると出力は同じですが、say1, say2, say3がそれぞれ0.1秒刻みで出てきます。
setTimeoutという関数によるものです。setTimeoutとは、タイマーをセットしてくれます。これによって非同期での処理が実現できます。

非同期処理とはこんな感じで順番とか待ち時間とかバラバラにして実装して、空いてる時間に違うタスクをこなすことができる便利な方法である!ということで話を進めていきたいと思います。

少しコードを書いてみる

上で何も言わずにさらっと書いてしまいましたが、JavaScriptで非同期処理を行うときにはいくつか方法があります。

例として、以下のようなものが大変有名です。

  • Promise
  • async/await
  • setTimeout

それぞれについて説明をしていきたいと思います。

Promise

Promiseは非同期処理の最終的な成功、または失敗を表すためのオブジェクトです。第一引数に上手くいったときに実行する処理を、第二引数には失敗したときに実行する処理を記述します。
これを利用することで、非同期のアクションの成功や失敗に対するハンドラを関連づけることができます。未来のある時点でのプロセスを返すことで同期メソッドと同じように値を返すことができます。

Promiseには3つの状態があります。

  • 待機: 初期状態
  • 満足: 処理が成功して完了したこと
  • 拒絶: 処理が失敗したこと

実際に簡単なコードを書いてみます。

new Promise((resolve, reject) => {
    console.log("初期");
    resolve();
})
.then(() => {
    throw new Error("Something failed");
    console.log("失敗したとき(reject)");
})
.catch(() => {
    console.log("実行1");
})
.then(() => {
    console.log("実行2");
});

これの出力は以下のようになります。

初期
実行1
実行2

こんな感じになります。'失敗したとき(reject)'は表示されません。これは失敗したときにだけ実行されます。

プロミスについてもっと詳しく知りたい方はこちら

async/await

async/awaitとは、JavaScriptの非同期通信を行うためのものです。Reactとか使う人はAPI叩くときによく使うのでご存知かと思います。ECMAScript2017で追加されました。

少しだけコードを書いてみたいと思います。

const sleep = async (msec) => {
    var start = new Date().getTime()
    while(new Date().getTime() < start + msec);
}

const printFunction = async (str) => {
    await sleep(1000)
    console.log(str)
}

printFunction("hogehoge")

少々苦しいコードになってしまいました。(なんかいいのあったらリクエストください)
これを実行すると1秒待ってからhogehogeが表示されます。あまり実感しにくいですし、普通は次にでてくるsetTimeoutを使用して実装することが多いためわけわからなくなってますが、一応これは非同期で実行することができています。async/awaitで注意すべき点は、async functionを実行するときにはawaitをつけなければいけませんし、逆も然りです。
SyntaxError: await is only valid in async function
と怒られてしまいます。

これらはセットで使うと覚えておくといいと思います。

setTimeout

setTimeoutはプロセスのスケジューリングをするための関数です。指定時間経過後に一度だけ関数を実行するときに使用されます。第一引数に実行したい関数、第二引数に待たせたい時間を渡します。

これを使用するとさっきのasync/awaitと同じようなことができます。

const printFunction = () => {
    console.log("hogehoge")
}

const testFunction = () => {
    setTimeout(printFunction, 1000)
}

testFunction()

これで先ほどと同じように実行してから1秒後にhogehogeを表示するためのプログラムを書くことができます。

ここで注意すべきことがあります。

  • 第一引数は関数だが()はいらない
    • ここに()をつけてしまうと関数を渡しているのではなく実行していることになってしまうため()入らない
    • あくまでも関数を渡すだけで実行は別のところで行われる(サーバ側)
  • 第二引数のデフォルト値は0

ここに注意しながら実装した方が良さそうです。WebではDOMの更新が頻繁に行われる場合などがありますが、そのようなときにsetTimeoutを使用することが多いです。そのような時は第二引数を省略したり、0にしたりすることが大切になります。Webに上手く活かすことを意識してコーディングをすると生産性が上がること間違いなしです。

全てを総合すると

上で書いたそれぞれのことを踏まえた上で簡単に実装を行ってみます。

const waitTime = async (msec) => {
    const promise = new Promise((resolve) => setTimeout(() => resolve(), msec))
    return promise
}

const doAsyncTask1 = async () => {
    await waitTime(1000)
    return 'doAsyncTask1'
}

// doAsyncTask1の戻り値を引数に入れる
const doAsyncTask2 = async (value, ) => {
    console.log("doAsyncTask2 value: " + value);
    await waitTime(1000)
    return 'doAsyncTask2'
}

// doAsyncTask2の戻り値を引数に入れる
const doAsyncTask3 = async (value, ) => {
    console.log("doAsyncTask3 value: " + value);
    await waitTime(1000)
    return 'doAsyncTask3'
}

const failureHandler = async (error) => {
    console.log("error: " + error);
}

// 実行
(async function() {
    try {
        const result = await doAsyncTask1() // doAsyncTask1の戻り値をresultへ
        const newResult = await doAsyncTask2(result) // それを渡す
        const finalResult = await doAsyncTask3(newResult) // 同じ
        console.log('final result: ' + finalResult) // 終わったらログを出力する
    } catch(error) {
        failureHandler(error);
    }
})();

Promiseの生成は new Promise((resolve) => setTimeout(() => resolve(), msec)) で行っています。
これの場合は、resolveを引数として取り、処理が成功したときにsetTimeoutが呼ばれ、引数で与えたmsec秒待つという処理になっています。これらを全て非同期で処理するためにasync/awaitで定義し、実行しています。

ちょっぴり実践的な内容

最後にちょっとだけ実践的な内容に触れてみたいと思います。
ここでは僕のブログのフロントエンドのコードを例として扱います。
全てのコードはこちらにあります。

ここのレポジトリの中に /src/api/getAPI.jsというファイルがあります。
ここでは、投稿一覧や投稿詳細を取得するためのGETリクエストを投げるコードが書いてあります。少しだけ中を覗いてみましょう。

getAPI.js
// JSONに変換してコンポーネントに渡すための関数
const toJson = async (res) => {
    const js = await res.json()
    if (res.ok) {
        return js
    } else {
        throw new Error(js.message)
    }
}

// 投稿一覧を取得するための関数
export const getMainAPI = async () => {
    const resp = await fetch(`https://takurinton.com/blog`, {
        method: "GET",
        credentials: "same-origin",
    })
    return await toJson(resp)
}


export const getMainAPILocal = async () => {
    const resp = await fetch(`http://localhost:8000/blog`, {
        method: "GET",
        credentials: "same-origin",
    })
    return await toJson(resp)
}

// 投稿詳細を取得するための関数
export const getDetailAPI = async (id) => {
    const resp = await fetch(`https://takurinton.com/detail/${id}/`, {
        method: "GET",
        credentials: "same-origin",
    })
    return await toJson(resp)
}

export const getDetailAPILocal = async (id) => {
    const resp = await fetch(`http://localhost:8000/blog/${id}/`, {
        method: "GET",
        credentials: "same-origin",
    })
    return await toJson(resp)
}

全てasync/awaitの非同期通信で処理を行っています。
テスト環境と本番環境で分けてますが、中身は同じです。あまりやってることは難しくないと思います。さっきまでの方が難しい気がします。

これを受け取って表示するためのコンポーネントは /src/components/Post.jsx になります。

Post.jsx
import React, {useEffect, useState}  from 'react';

import {getMainAPILocal, getMainAPI} from '../api/getAPI'

const PostContent = (post) => {
    const domain = 'https://takurinton.com'
    const dateList = post.pub_date.split('-')
    const pubDate = post.pub_date
    return(
        <div className="top">
            <a href={post.id}>
                <h1>{post.title} </h1>
                <img src={domain+post.contents_image_url} alt=""/>
                <p>{pubDate}</p>
            </a>
            <hr />
        </div>
    )
}

const Post = () => {
    const [post, setPost] = useState([])
    useEffect(async () => {
        const post = await getMainAPI()
        setPost(post)
    },[])

    return(
        <div className="main">
            {post.map(i => PostContent(i))}
        </div>
    )
}

export default Post

本質から逸れてしまうのであまり詳しいことは話しませんが、Postという関数の中でまたawaitを使用して実行し、投稿の一覧を取得しています。このようにして予想よりも簡単に実装をすることができるとわかったと思います。

まとめ

今回Promise, async/await, setTimeoutについて話しましたが、こやつらはセットで使うことが多いのでそれぞれ別に説明したことはちょっと失敗だったかなとか思ってます。Webのフロントを書く上でもこのような知識は必要不可欠なため、知っておいて損はしませんし、自分自身もこれらを切り離して考えたことがなかったのでとてもいい機会になりました。

JavaScriptはフロントエンドにもバックエンドにも使用することができるとても良い言語だと思います。歴史のある言語ですがまだまだ伸び代が残っていてとても面白いです!
これからもゆるふわ言語エンジニアとして活動していく上でJavaScriptは必要不可欠なのでもっと勉強して強くなります!

皆さんもJavaScriptと仲良くなりましょう!

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

JavaScript カウントダウンタイマー

数年前の作りかけスクリプトを見つけたのでとりあえず動くようにしてみました。
01.jpg
計測したい時間は数字ボタンで入力します。
例えば15分計りたい場合は[1][5][分][スタート]でも[9][0][0][秒][スタート]でも[.][2][5][時間][スタート]でも構いません。

計測中および一時停止中どちらでも、数字ボタンからの入力で計測時間の追加が行なえます。

計測中にブラウザを閉じてしまっても再度画面を表示させた際にまだ計測終了時間に達していなければ続行が可能ですが、閉じている間に計測終了時間になっても通知はできません。

動作サンプルはこちら

HTML

index.html
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<title>Countdown Timer</title>
<meta name='viewport' content='user-scalable=no,width=device-width,initial-scale=1'>
<script src='./timer.js' type='text/javascript'></script>
<link rel='stylesheet' type='text/css' href='./timer.css' media='all'>
</head>
<body width='100%' id='mainArea'>

<div class='block1'>
    <div class='blockA'>
        <input type='text' value='0' id='d0' class='setNum' readonly><br />
        <input type='button' value='7' id='k7' class='numKey' onclick='setNum(7)'><input type='button' value='8' id='k8' class='numKey' onclick='setNum(8)'><input type='button' value='9' id='k9' class='numKey' onclick='setNum(9)'><br />
        <input type='button' value='4' id='k4' class='numKey' onclick='setNum(4)'><input type='button' value='5' id='k5' class='numKey' onclick='setNum(5)'><input type='button' value='6' id='k6' class='numKey' onclick='setNum(6)'><br />
        <input type='button' value='1' id='k1' class='numKey' onclick='setNum(1)'><input type='button' value='2' id='k2' class='numKey' onclick='setNum(2)'><input type='button' value='3' id='k3' class='numKey' onclick='setNum(3)'><br />
        <input type='button' value='0' id='k0' class='numKey' onclick='setNum(0)'><input type='button' value='.' id='kp' class='numKey' onclick='setNum(".")'><input type='button' value='C' id='kr' class='numKey clearButton' onclick='reset()'><br />

        <input type='button' value='時間' class='numKey timeButton' onclick='addTotal("h")' id='kh'><input type='button' value='分' class='numKey timeButton' onclick='addTotal("m")' id='km'><input type='button' value='秒' class='numKey timeButton' onclick='addTotal("s")' id='ks'>
    </div>
    <br />
    <div class='blockB'>
        <input type='text' value='0:00:00.0' id='d1' class='progressTime' readonly>

        <div id='progressArea'>
            <span id='progressBar'></span>
        </div>

        <span class='buttonArea'>
            <input type='button' value='リセット' id='kac' class='resetButton' onclick='allClear()'>
            <input type='button' value='スタート' id='kss' class='startButton' onclick='countStart()'>
        </span>

    </div>
</div>

<input type='button' value='' id='krs' class='resumeButton'>

<script type='text/javascript'>
    var windowAudioContext = window.AudioContext || window.webkitAudioContext;
    if(windowAudioContext === undefined) {
        var audioTag = ''+
        '<audio id="seClick" preload="auto">'+
        '   <source src="sound/click.mp3" type="audio/mp3">'+
        '</audio>'+
        '<audio id="seAlarm" preload="auto">'+
        '   <source src="sound/alarm.mp3" type="audio/mp3">'+
        '</audio>'+
        '';
        document.write(audioTag);
    }
</script>

<span style='display:none;'>
<input type='button' id='se1' value='se1'>
<input type='button' id='se2' value='se2'>
</span>

</body>
</html>

JavaScript

timer.js
'use strict';

let totalSec = 0;
let allTotalSec = 0;
let countDownSec = 0;
let startMs = 0;
let countId;
let endId;

const d = function(id) { return document.getElementById(id);}

const seIds = [
    'seClick',
    'seAlarm'
];

window.AudioContext = window.AudioContext || window.webkitAudioContext;
let context;
if(window.AudioContext !== undefined) {
    context = new AudioContext();
}

const seBufferList = [
    'sound/click.mp3',
    'sound/alarm.mp3'
];
let webSoundApiFlg = 0;

const getAudioBuffer = function(url, fn) {
    const req = new XMLHttpRequest();
    req.responseType = 'arraybuffer';
    req.onreadystatechange = function() {
        if (req.readyState === 4) {
            if (req.status === 0 || req.status === 200) {
                context.decodeAudioData(req.response, function(buffer) {
                    fn(buffer);
                });
            }
        }
    };
    req.open('GET', url, true);
    req.send('');
};

const playSound = function(buffer) {
    const source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.start(0);
};

function seLoad(n) {
    getAudioBuffer(seBufferList[n] + '?200812', function(buffer) {
        const btn = d('se' + (n + 1));
        btn.onclick = function() {
            playSound(buffer);
        };
    });
}

window.onload = function() {
    if(context !== undefined) {
        for(let i = 0; i < seBufferList.length; i++) seLoad(i);
        webSoundApiFlg = 1;
    }

    document.addEventListener("touchmove", function(event) {
        event.preventDefault();
    }, { passive: false });

    const storage = localStorage.getItem('timer_params');
    const params = storage !== null ? JSON.parse(storage) : {};

    if(params.allTotalSec !== undefined && params.allTotalSec > 0) {
        allTotalSec = params.allTotalSec;
        totalSec = params.totalSec;
        startMs = params.startMs;

        d('krs').style.display = 'block';

        let tmpId = setInterval(function() {
            let progressSec = (Date.now() - startMs) / 1000;
            countDownSec = params.pause !== undefined ?
                totalSec :
                totalSec - progressSec;
            if(countDownSec <= 0) {
                clearInterval(tmpId);
                d('krs').style.display = 'none';
                allTotalSec =
                totalSec =
                startMs = 0;
                localStorage.removeItem('timer_params');
            }
            d('krs').value =
                (params.pause !== undefined ? '一時停止中 ' : '') +
                timeString(countDownSec) + ':画面を' +
                (window.ontouchstart !== undefined ? 'タップ' : 'クリック') +'で復帰できます';
        }, 100);

        d('krs').addEventListener('click', function() {
            clearInterval(tmpId);
            d('krs').style.display = 'none';
            let progressSec = (Date.now() - startMs) / 1000;
            countDownSec = params.pause !== undefined ?
                totalSec :
                totalSec - progressSec;

            if(countDownSec > 0) {
                se(1);
                if(params.pause !== undefined) {
                    d('d1').value = timeString(totalSec) + '.' +
                        (totalSec % 1).toFixed(1).slice(-1);
                } else {
                    d('kss').value = 'ストップ';
                    countId = setInterval('progressCount()', 25);
                }
            }
        }, false);
    }
};

function se(num) {
    if(webSoundApiFlg) {
        d('se' + num).onclick();
    } else {
        const id= (seIds[num-1] !== undefined) ? seIds[num-1] : '';
        if(id) {
            const _d = d(id);
            if(_d.currentTime != 0 || !_d.paused) {
                _d.pause();
                _d.currentTime = 0;
            }
            _d.play();
        }
    }
}

function setNum(n) {
    let d0 = d('d0').value;
    if(n == '.' && d0.match(/\./)) return;

    d0 += String(n);
    d0 = d0.replace(/^0+$/, '0')
           .replace(/^(\.)/, '0.')
           .replace(/^0(\d+)/, "$1");

    d('d0').value = d0;
    se(1);
}

function setDisp(s) {
    s = s.replace(/^0+$/, '0');
    if(s != '0') s = s.replace(/^0+/, '');

    const s0 = s;
    const c = [];
    while(s != '' && c.length < 2) {
        c.push(s.substr(-2));
        s = s.substr(0,s.length-2);
    }
    if(s) c.push(s);
    d('d0').value = s0;
}

function reset() {
    d('d0').value = '0';
    se(1);
}

function addTotal(m) {
    const d0 = d('d0').value;

    let plusSec = 0;
    if(m == 'h') {
        plusSec = Math.floor(d0 * 3600);
    } else if(m == 'm') {
        plusSec = Math.floor(d0 * 60);
    } else {
        plusSec = Math.floor(d0 * 10) / 10;
    }

    totalSec += plusSec;
    allTotalSec += plusSec;

    const params = {
        allTotalSec: allTotalSec,
        totalSec: totalSec,
        startMs: startMs,
    };
    if(!countId) params.pause = 1;
    localStorage.setItem('timer_params', JSON.stringify(params));

    d('d0').value = '0';
    const parsent = totalSec / allTotalSec * 100;

    se(1);

    if(countId) return;
    d('progressBar').style.width = parsent + '%';

    const dH = Math.floor(totalSec / 3600);
    const dM = Math.floor(totalSec / 60) % 60;
    const dS = Math.floor(totalSec % 60);
    d('d1').value =
        dH + ':' +
        ('0' + dM).substr(-2) + ':' +
        ('0' + dS).substr(-2) + '.' +
        (totalSec % 1).toFixed(1).slice(-1);
}

function allClear() {
    totalSec = 0;
    allTotalSec = 0;

    d('d0').value = '0';
    d('d1').value = '0:00:00.0';
    d('progressBar').style.width = '0px';

    localStorage.removeItem('timer_params');
    if(countId) {
        clearInterval(countId);
        countId = '';
        d('kss').value = 'スタート';
    }
}

function countStart() {
    if(totalSec <= 0) return;
    se(1);

    if(countId) {
        clearInterval(countId);
        countId = '';
        totalSec -= ((Date.now() - startMs) / 1000 );
        d('kss').value = 'スタート';
        localStorage.setItem('timer_params', JSON.stringify({
            allTotalSec: allTotalSec,
            totalSec: totalSec,
            startMs: startMs,
            pause: 1,
        }));
        return;
    }

    d('kss').value = 'ストップ';
    startMs = Date.now();
    if(!countId) countId = setInterval('progressCount()', 25);

    localStorage.setItem('timer_params', JSON.stringify({
        allTotalSec: allTotalSec,
        totalSec: totalSec,
        startMs: startMs,
    }));
}

function progressCount() {
    const progressSec = (Date.now() - startMs) / 1000;

    countDownSec = totalSec - progressSec;
    d('d1').value =
        timeString(countDownSec) + '.'+ (countDownSec % 1).toFixed(1).slice(-1);

    const parsent = (totalSec - progressSec) / allTotalSec * 100;
    d('progressBar').style.width = parsent + '%';

    if(countDownSec <= 0) {
        clearInterval(countId);
        countId = '';
        d('kss').value = 'スタート';
        d('progressBar').style.width = '0px';
        totalSec = 0;
        allTotalSec = 0;
        d('d1').value = '0:00:00.0';
        timeUp();
    }
}

function timeString(sec) {
    sec = Math.floor(sec);
    return Math.floor(sec / 3600) + ':' +
        ('0'+ Math.floor(sec / 60) % 60).slice(-2) + ':' +
        ('0' + (sec % 60)).slice(-2);
}

function timeUp() {
    localStorage.removeItem('timer_params');
    se(2);
    endId = setInterval('alarm()', 1000);
    alarmCount = 0;
}

function alarm() {
    if(window.ontouchstart === undefined) {
        document.onmousedown = function(e) {
            if(e.type == 'mousedown') {
                if(endId) clearInterval(endId);
                return;
            }
        }
    } else {
        d('mainArea').ontouchstart = function() {
            if(endId) clearInterval(endId);
            return;
        }
    }
    se(2);
}

CSS

timer.css
@media only screen and (orientation : portrait) {
    .setNum {
        text-align: right;
        width: 100%;
        font-size: 20px;
        border-radius: 6px;
    }

    #progressArea {
        display: table;
        background-color: #cceeff;
        border-radius: 6px;
        border: 1px #555588 solid;
        width: 100%;
        height: 16px;
        overflow: hidden;
        margin-top: 4px;
    }
    #progressArea #progressBar {
        display: inline-block;
        background-color: #4488ff;
        width: 0px;
        height: 100%;
    }

    .numKey {
        width: 33.33%;
        height: 8%;
        font-size: 20px;
        border-radius: 6px;
        border: 1px #444444 solid;
    }

    .progressTime {
        text-align: right;
        width: 100%;
        font-size: 45px;
        padding-right: 10px;
        border-radius: 6px;
    }

    .buttonArea {
        text-align: center;
        display: block;
        margin-top: 5px;
    }
    .startButton {
        width: 60%;
        height: 12%;
        font-size: 16px;
    }
    .resetButton {
        width: 36%;
        height: 12%;
        font-size: 16px;
    }
}

@media only screen and (orientation : landscape) {
    .block1 {
        display: inline-flex;
        width: 100%;
        height: 100%;
    }
    .blockA {
        vertical-align: top;
        width: 40%;
        max-width: 320px;
    }
    .blockB {
        vertical-align: top;
        margin-left: 20px;
    }

    .setNum {
        text-align: right;
        width: 100%;
        font-size: 20px;
        border-radius: 6px;
        max-width: 320px;
    }
    .numKey {
        width: 33.33%;
        height: 14%;
        font-size: 20px;
        border-radius: 6px;
        border: 1px #444444 solid;
        max-width: 106.6px;
        max-height: 80px;
    }

    .progressTime {
        text-align: right;
        width: 100%;
        font-size: 45px;
        padding-right: 10px;
        border-radius: 6px;
        max-width: 320px;
    }
    #progressArea {
        display: table;
        background-color: #cceeff;
        border-radius: 6px;
        border: 1px #555588 solid;
        width: 100%;
        height: 16px;
        overflow: hidden;
        margin-top: 4px;
        max-width: 320px;
    }
    #progressArea #progressBar {
        display: inline-block;
        background-color: #4488ff;
        width: 0px;
        height: 100%;
    }

    .buttonArea {
        text-align: center;
        display: block;
        margin-top: 5px;
        max-width: 320px;
    }
    .startButton {
        width: 100%;
        height: 60px;
        font-size: 20px;
        border-radius: 6px;
    }
    .resetButton {
        width: 100%;
        height: 40px;
        font-size: 16px;
        border-radius: 6px;
    }
}

#progressArea {
    text-align: left;
}

html {
    touch-action: manipulation;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
}

input {
    -webkit-appearance: none;
    -webkit-user-select: none;
}

.numKey {
    background-color: #fafafa;
}
.clearButton {
    background-color: #e0e0e0;
}
.timeButton {
    background-color: #d0d0d0;
}

.startButton {
    background-color: #e0ffe0;
}
.resetButton {
    background-color: #ffa0a0;
}

#krs {
    display: none;
    position: fixed;
    width: 100%;
    height: 100%;
    background: #fffe;
    top: 0px;
    left: 0px;
    font-size: 20px;
    border: 0px;
    z-index: 10;
    white-space: pre-wrap;
}

古い書き方をしている部分もありますが、IE11でも動くのでとりあえずそのままにしておきました。

操作音、アラームはそれぞれsoundディレクトリに入れたclick.mp3alarm.mp3を鳴らすようにしてあります。

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

光センサで遊ぶ「かくれんぼ」をobnizとp5jsで作ってみた

かくれんぼしたい

皆さんは小学生の頃は何をして遊ぶ子供でしたか?
僕はかくれんぼ(with DSのピクトチャット)ドッジボールデュエルマスターズをやっていました。
今考えると体力本当に無限大だったなあと懐かしい気分になります。

先日人生初のハッカソンに出場しまして、あるチームがobnizとp5jsとを組み合わせたゲームを作っていました。
obnizで物理的な動作を検知しインタラクティブにp5jsで描画する、という発想に関心が沸き、今回は勉強もかねて自宅でも全力でかくれんぼできるゲームをp5jsで作ってみました。
ただのかくれんぼでもつまらないので、背景に隠れながら逃げるかくれんぼを作ってみました。

コードはGistに載せてありますので是非是非実装してみてくださいね。
アプリもデプロイしたのでURLから遊びに行ってみてください。
obnizがあっても無くても遊べるようにしてます。

コード全体は⇒こちら

光センサで遊ぶなら⇒こちら
超音波センサで遊ぶなら⇒こちら
obniz無しで遊ぶなら⇒こちら
※配線情報は後ほどの章で掲載しているので確認よろしくお願いします!

完成デモ

基本ルール

かくれんぼと同じく、鬼から隠れつつ逃げつつ生き延びていくゲームとなっています。
ゲームの特徴として、プレイヤーは自身の色を操作することができ、背景の色に隠れることができます
何日まで生き延びたらクリアとかはなく、タッチされるまでの最長生存記録を目指すものとなっています。

鬼はプレイヤーが近くにいたり、背景から浮いていたら全力で追いかけてきます
鬼の色が真っ黒なので、夜の時間はプレイヤーも鬼もものすごく見にくくなります。
その間も鬼は動き回るので、夜の間につかまってしまうことが大多数だと思います。
いかに夜をうまく超えるか、という部分がポイントのゲームになっています。

また、一日過ぎるごとに鬼はスピードが上がっていきます
一日目はゆるゆる動く鬼ですが、五日目ぐらいになると最初の二倍近い速さで動き回ることになります。
その鬼からいかに隠れて逃げ回るか戦略が試されるゲームとなっています。

プレイヤー操作

プレイヤー移動

プレイヤー移動はキーボードの十字キーとなってます。
右手はキーボードでプレイヤー移動、左手でプレイヤーの色操作が基本になっています。

プレイヤー色操作

プレイヤーの色操作はやり方が3通りありまして、
・光センサー
・超音波センサー
・キーボード操作
となっています。
それぞれ紹介していきますね。

光センサー

まずはコンセプトに沿うように光センサーで操作する方法です。
光センサーを手で覆うとプレイヤーが黒くなり、手を離すとプレイヤーが白くなります。
程よい色を維持する場合は、絶妙に光を入れ続ける必要があり、なかなか高難易度になっています。

今回使ったセンサーはCdSセル5mmタイプ、抵抗は330Ωを使っています。(参考:秋月電子)
写真のように配線をしてご利用下さい(赤:0へ、白:1へ、黒:2へ接続)。DSC_0066.JPG
DSC_0067.JPG

光センサを利用したゲーム入り口は⇒こちら

超音波センサー

次に超音波センサーを利用した方法です。
なぜ超音波センサー?と思ったかもしれませんが、当初「obnizとp5jsのインタラクティブなゲーム」とだけ考えており、超音波センサーを使おうが最初に思い浮かびました。
その中で「あれ光センサー使ったほうがコンセプト通りじゃね?」となり、両方実装しました。
超音波センサーから物体への距離が小さくなるとプレイヤーは黒くなり、距離が大きくなるとプレイヤーは白くなります。

今回使った超音波センサーはHC-SR04です。(参考:秋月電子)
超音波センサーは配線というよりはそのままobnizに接続すると操作しやすかったです。

DSC_0068.JPG

超音波センサーを利用したゲーム入り口は⇒こちら

キーボード操作

obniz持っていない方、このセンサー類を持っていない方に、obnizを使わないでキーボード操作のみでもできる設定を用意しています。
プレイヤーの色調整は
 Aキー:影に近づく(黒くなる)
 Fキー:光に近づく(白くなる)

となっております。

obnizでの色調整は結構繊細なので難しく、最初はこちらで要領をつかむのが良いかもしれません。
是非キーボードオンリーでも挑戦してみてくださいね。
キーボードオンリーのゲーム入り口は⇒こちら

実装こまごま

ブラウザとobnizの連携

node.jsでobnizと接続したことはありましたが、html中にjsとして埋め込むのは初めてでした。
html部分の実装はこちらです。
↓こちらをかなり参考にさせていただきました。
https://qiita.com/JINPLAYSGUITAR/items/3bcee02d8a612544c06e

<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  <script src="https://unpkg.com/obniz/obniz.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.1/p5.js"></script>
</head>
<body>
<div id="obniz-debug"></div>
<script>

//ここに処理を書く

</script>
</body>
</html>

デプロイして他の人に自分自身のobnizで楽しんでもらいたいと思いました。
html側でinputとかbuttonとかいるのかな?と思っていたんですがobnizオブジェクトを作ったときに勝手にポップアップを表示してくれるようです。
もしかしたらobniz_idの一文すらいらないかも・・・?

    //obniz Idをインプットしてもらう
    var obniz_id = document.getElementsByName("obniz_id");
    var obniz = new Obniz(obniz_id);

    //照度の取得
    var hikari = 0.2;
    obniz.onconnect = async () => {
    obniz.io0.output(true); //io0を5vに(電気を流す)
    obniz.io2.output(false); //io2をGNDに(電気を逃がす)

    //io1をアナログピンに(センサーの値を取得)
    obniz.ad1.start((voltage) => {
        //センサーの値が変わるたびに実行される
        hikari = voltage;
    });
}

obnizとp5jsとの連携

最初はobnizと連携した後にp5jsを描画するので、obniz.onconnect = async () =>の後に描画処理をいれるのかな?と思っていました。
処理もobniz.onconnectのところで止まってしまうんじゃないか・・・?と思って色々試行錯誤しました。

    //obniz Idをインプットしてもらう
    var obniz_id = document.getElementsByName("obniz_id");
    var obniz = new Obniz(obniz_id);

    obniz.onconnect = async () => {
    //処理がここで止まって
    });
}

//ここから先の処理が進まないんじゃないか?と思ってました

試行錯誤の結果、どうもきちんとonconnect以降の処理も走ってくれるようです。
obnizの処理とゲーム側の処理を別々に書いても問題ない様子。
これはらくちんですね。

    //obniz Idをインプットしてもらう
    var obniz_id = document.getElementsByName("obniz_id");
    var obniz = new Obniz(obniz_id);

    //照度の取得
    obniz.onconnect = async () => {
      //ここを処理しつつ
    });
}
    //ここ以降も処理を走らせてくれるみたいです
    function setup(){
    //読み込み時の処理
    }
    function init(){
    //ゲームの初期化の処理
    }
    function draw(){
    //ゲーム中、常にここの処理が実行
    }

ただ変数のスコープで混乱しないように注意してくださいね。
今回色々試行錯誤する中で一番多かったのが変数のスコープの定義ミスでした。
もし思い通りの動作をしないなと思ったときは、今一度変数のスコープを確認してみてください!

コードの全体はGist(こちらから)に載せてあるので、是非ご覧になってくださいね。

今後やりたいこと

p5js面白い!色んな可能性を感じるライブラリですね。
最近ブラウザを利用すればスマホの各種センサ類にアクセスできることを勉強したので、スマホの加速度センサとか使いながら何かゲーム作れないかなとか考えています。

最後までご覧いただきありがとうございました!
LGTMつけていただけると励みになりますので、よろしくお願いいたします!

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

オープンソースとプライバシー志向の分析ソリューション

data-privacy-meme.png

はじめに

こんにちは streampack チームのメディです。
https://cloudpack.jp/service/option/streampack.html

Objective ・ 目的

Privacy is an important matter.
I would like to present several open source solutions that respect users privacy and will allow you to improve your business.

プライバシーは重要な問題です。
ユーザーのプライバシーを尊重し、ビジネスの改善を可能にするいくつかのオープンソースソリューションを紹介したいと思います。

Disclaimer ・ 免責事項

Analytics systems are listed in no particular order.
分析システムは特定の順序でリストされていません。

Offen

Screen Shot 2020-07-16 at 10.49.56.png

From the homepage ・ ホームページから

Offen is a fair and open alternative to common web analytics tools. Gain insights while your users have full access to their data. Lightweight, self hosted and free.

Offenは、一般的なWeb分析ツールの公正でオープンな代替手段です。 ユーザーがデータに完全にアクセスしながら、洞察を得ます。 また、軽量、自己ホスト型で、無料です。

Key points

  • Privacy friendly
  • Transparent and fair
  • lightweight

主な機能

  • プライバシーフレンドリー
  • 透明かつ公正
  • 軽量

https://github.com/offen/offen

PostHog

dashboards-0309f28b408dfd06dcb58fe11fefd185.png

From the homepage ・ ホームページから

Understand your users.
Build a better product.

ユーザーを理解します。
より良い製品を作ります。

Key points

  • privacy-focused
  • detailed product analytics

主な機能

  • プライバシー志向
  • 詳細な製品分析

https://github.com/posthog/posthog

Matomo (formely Piwik)

website-graphics-2020-v5-1.png

From the homepage ・ ホームページから

Google Analytics alternative that protects your data and your customers' privacy.

データと顧客のプライバシーを保護するGoogleアナリティクスの代替手段。

Key points

  • Real time analysis
  • Customizable

主な機能

  • リアルタイム分析
  • カスタマイズ可能

https://github.com/matomo-org/matomo

Fathom

fathom.jpg

From the homepage ・ ホームページから

Fathom Analytics is a simpler and more privacy-focused alternative to Google Analytics.

Fathom Analyticsは、Google Analyticsに代わる、よりシンプルでプライバシー重視の代替手段です。

Key points:

  • Simple website analysis
  • Do not use cookies

主な機能

  • シンプルなウェブサイト分析
  • クッキーを使用しない

https://github.com/usefathom/fathom

Information sources ・ 情報元

https://github.com/offen/offen
https://github.com/usefathom/fathom
https://github.com/posthog/posthog
https://github.com/matomo-org/matomo

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

スマホもPCの一員としてファイル同期したいじゃない?Ⅱ

おはようございます!

前回の続きです。

前回の

Syncthingに似てる気がするけど知ってる?参考になるかもよー見てみたら??」

にスマホアプリも追従させてみたよ!って記事です。

やたらスマホ!

要するにHTTP(S)使ってフォルダを同期してくれる君というやつです。
Syncthing以外にもNearby Shareもあるじゃーんってご指摘あるな。
でも、これ強みとしてはバイナリいっこで動くし、マルチOS対応だし、、

あと今あるプロキシとかHTTPエコシステムに乗っかれるのがこのツールの推しポイント

DE・KI・TA

お告げ通り機能追加しましたYO

  • ディレクトリのあるフォルダでも再帰的に同期
  • 前回同期時の設定をコンフィグに出して、次回はそれ読んで起動
  • 同期状況を表示する
  • HTTPS同期をサポート

詳しくはリポジトリ見てくださいな。releaseの.apkをもってくればインスコできます。

2が付いてないリポジトリは非互換なので注意です。

つかいたい!

  • PC版を起動したら、EnterかSpaceを押すとQRコードが表示されます

1.png

  • それをスマホ版アプリで読み取れば同期が開始されます

UIわからん!

screen.png

①は同期ゲージです。この数字が何秒毎に同期するかになります。同期間隔が狭いとデカいファイルが転送しきれないのでその点はご注意を

②同期先を変えるときなど。QR Codeをもう一度読みましょう

③保存先フォルダを変更します

④は同期した時にサーバーに無いのに、クライアントにあるファイルを消すモードです。完全同期しておきたい時に使います。

⑤アプリを終了して設定をコンフィグファイルに書き出します

⑥は同期状況とか動作ステータスが色々でます

あとがき

GUI、転送効率化プロトコル実装、中継サーバーとかsyncthingにある機能で
実装出来てないとこはあるけど、その他の最低限は盛り込めたかな?

Win、Mac以外にもラズパイとか色々持ってる人は便利だと思うので
是非使ってみてくださいな。(そんな人のニッチだよ!ってw)

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

BottomNavigationViewを少しいじってみた②

■ 各itemクリック時のviewにfragmentとxmlを使って分ける

bottomnavigationviewのitemを押したときのviewを、それぞれのレイアウトを別のものにしたかったため、fragmentとxmlでわけました。

main.java
package com.example.yoshihiro.smartkoneco;

import ...

public class Main extends AppCompatActivity {

    private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
            = new BottomNavigationView.OnNavigationItemSelectedListener() {

        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            switch (item.getItemId()) {
                case R.id.fragment1:
                    loadFragment1();
                    return true;
                case R.id.fragment2:
                    loadFragment2();
                    return true;
                case R.id.fragment3:
                    loadFragment3();
                    return true;
                case R.id.fragment4:
                    loadFragment4();
                    return true;
            }
            return false;
        }

    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        if (savedInstanceState == null) {
            Fragment1();
        }
        BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
        disableShiftMode(navigation);
        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
    }

    public static void disableShiftMode(BottomNavigationView view) {
        ...
    }

    private void loadFragment1() {
        Fragment1 fragment = Fragment1.newInstance();
        FragmentTransaction ft = getFragmentManager().beginTransaction();
        ft.replace(R.id.fragment_frame, fragment);
        ft.commit();
    }
    // 以下load~ 3つで同じ
}

class Fragment1 extends Fragment {
    public static Fragment1 newInstance() {
        return new Fragment1();
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment1, container, false);
    }
}
// 以下3つfragment2~4で同じ
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.yoshihiro.smartkoneco.Main">

    <include layout="@layout/main_title"
        tools:layout_editor_absoluteY="-44dp"
        tools:layout_editor_absoluteX="-260dp" />

    <FrameLayout
        android:id="@+id/fragment_frame"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
    </FrameLayout>

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="?android:attr/windowBackground"
        app:itemIconTint="@color/buttom_navigation"
        app:itemTextColor="@color/buttom_navigation"
        app:menu="@menu/navigation" />

</LinearLayout>
time_table.xml,record.xml,room_search.xml,news.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!--
        各レイアウト
    -->
</LinearLayout>

参考:

Android using BottomNavigationView

Start fragment in BottomNavigationView

Bottom Navigation Views

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

Chromeページ翻訳でコードを翻訳させないスクリプトの作り方

やること

Chromeページ翻訳によって無残にも翻訳されてしまうReactプログラム...
スクリーンショット 2020-08-12 3.07.33.png

コードが翻訳されてしまうのは、該当箇所をHTMLのcodeタグで囲んでないからです。

今回は、こちらのサイトのコードが記載されてる部分をcodeタグでラップする簡単なJavaScriptを組んで、Chromeデベロッパーツールのconsoleから実行して対策してみようと思います。

JavaScriptの勉強がてら参考にどうぞ?‍♂️

実装

1. 状況を把握する

F12で検証ツールを開いて中身を覗くと、やっぱりcodeタグで囲まれてないっぽいですね。

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

今こんな感じなのを、

<pre class="mdxCodeBlock_iHAB">
    <div class="...">
        ...
    </div>
</pre>

こうすれば、大丈夫そうです。

<pre class="mdxCodeBlock_iHAB">
+   <code>
        <div class="...">
            ...
        </div>
+   </code>
</pre>

2. コーディング

コメント付きの完成版

// スコープを汚染しないために即時関数で囲んでおく
(() => {
  // 正規表現でclass名が 'mdxCodeBlock_' から始まるエレメントを全て取得
  const codeBlockElements = document.querySelectorAll("[class^=mdxCodeBlock_]");
  // エレメントごとに処理するためのループ
  codeBlockElements.forEach((element) => {
    // 子要素を削除する前に、コピーして残しておく
    // 子要素は1つしかないので、children[0]でOK
    // cloneNodeの引数は、deepcopyを有効にするか否か
    const copiedChildren = element.children[0].cloneNode(true);
    // 子要素を全削除
    while (element.firstChild) {
      element.removeChild(element.firstChild);
    }
    // codeエレメントを作成して、コピーしておいたエレメントをラップして元の場所に戻す
    const wrapElement = document.createElement("code");
    wrapElement.appendChild(copiedChildren);
    element.appendChild(wrapElement);
  });
})();

3. 適用

F12を押下して検証ツールを開いて、consoleタブに移動して今回のコードを叩きます。

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

実行後、ちゃんとcodeタグで囲まれてますね。

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

翻訳してもコードは無事です。(リロードのたびにスクリプトを実行する必要があります)

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

まとめ

こんな感じで同じようにやれば、他サイトも対応可能です。いちいちスクリプト実行するのが面倒であれば、Chrome拡張機能を自作して、ローカルで適用させれば永続化可能です。

ぜひ自力でやってみてください!!

ドキュメントオブジェクトモデル (DOM) - Web API | MDN

Twitter: @kzkzkazz

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