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

ストップウォッチ作ってみた(HTML,CSS,JavaScript)

ストップウォッチ作ってみた

HTML,CSS(SCSS),JavaScript(not jQuery)でストップウォッチ作ってみました。ストップウォッチ作成は初心者が通り道らしいので私も通ってみました。どこか変なところやアドバイスがあればコメントお願いしますー!

ちなみにSCSSは特に解説してません。

完成形

こんな感じのができました。0.5 倍で見ると見やすいです。CodePen の埋め込み機能を使ってみましたがデフォルトで 0.5 倍にできたらいいんですけどやり方わかりませんでした(笑)

See the Pen zYqGmJz by mkt-engr (@mkt-engr) on CodePen.

特長

  • 時間、分、秒、ミリ秒まで表示する(時間までは使われなそうやけど一応実装した)
  • START,STOP,RESET の 3 つのボタンがある
  • 最初は START ボタンのみ活性化されている
  • STOP ボタンを押すと START という文字が RESTART に変わる
  • STOP ボタンを押すと STOP ボタンが非活性化し RESTART ボタンと RESET ボタンが活性化される
  • RESET ボタンを押すと RESTART ボタンの文字が START に変わる

実装方針

new Date().getTime()がストップウォッチの主役です。MDN の getTime のページによると

1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒で表した数値。

とあるので

console.log(new Date().getTime());

とすると 1970 年 1 月 1 日 0 時 0 分からの経過時間をミリ秒で表示してくれます。これを利用して START ボタンや STOP ボタンを押した時刻を取得してストップウォッチを実装します。

ストップウォッチには START(RESTART),STOP,RESET の 3 つのボタンがありますがそれに対応して 3 つのイベントリスナーを実装します。

実装の詳細

一番 STOP ボタンの実装に苦労しました。

HTML と JavaScript のイベントリスナー以外の記述

HTML はシンプルです。時刻を表示する部分(<div class="display">)とボタンを表示する部分(<div class="buttons">)に分かれているだけです。

index.html
  <div class="stopwatch_wrapper">
    <div class="display">
      <span id="minutes" class="time">00</span>
      <span id="seconds" class="time">00</span>
      <span id="milli_seconds" class="time">000</span>
    </div>
    <div class="buttons">
      <button class="button" id="start">start</button>
      <button class="button" id="stop" disabled>stop</button>
      <button class="button" id="reset" disabled>reset</button>
    </div>
  </div>

ひとまずこれらのボタンや数値を操作するために以下の記述をします。

script.js
//上から分、秒、ミリ秒
const minutes = document.getElementById('minutes');
const seconds = document.getElementById('seconds');
const milli_seconds = document.getElementById('milli_seconds');

//ボタンたち
const start = document.getElementById('start');
const stop = document.getElementById('stop');
const reset = document.getElementById('reset');

3 つのボタンでそれぞれにイベントリスナーがついています。それらのイベントリスナーが共通でアクセスする変数を定義します。1 番下のpast_moving_timeがストップウォッチの実装の肝かなと思ってます。

script.js
// ストップウォッチを動かすときに用いるsetIntervalの返り値
let timer_id;

// ストップウォッチを動かし始めてからの時間
let stopwatch_time = 0;

// STARTボタンを押した時間
let press_start_time = 0;

// STOPボタンを押した時間
let press_stop_time = 0;

//ストップウォッチが動いていた時間の合計(STARTボタンを押してからSTOPボタンを押すまでの時間の合計)
let past_moving_time = 0;

3 つのイベントリスナー

START ボタン

START ボタンのイベントリスナーのコードは以下の通りです。3 つ特長があるのでそれはコードの後で書きます。

script.js
start.addEventListener('click', () => {
  press_start_time = new Date().getTime();
  timer_id = setInterval(() => {
    stopwatch_time = new Date().getTime() - press_start_time + past_moving_time;

    const time_milli_seconds = `00${Math.floor(stopwatch_time % 1000)}`.slice(
      -3
    );
    const time_seconds = `0${Math.floor((stopwatch_time / 1000) % 60)}`.slice(
      -2
    );
    const time_minutes = `0${
      Math.floor(stopwatch_time / 1000 / 60) % 60
    }`.slice(-2);
    const time_hours = `00${Math.floor(stopwatch_time / 1000 / 60 / 60)}`.slice(
      -3
    );

    //ブラウザに時間を描画する
    minutes.innerHTML = time_minutes;
    seconds.innerHTML = time_seconds;
    milli_seconds.innerHTML = time_milli_seconds;
  }, 1);

  1. START(RESTART)ボタンを押しからの経過時間の取得

まずボタンを押した時間を以下のようにして取得します。

   press_start_time = new Date().getTime();

現在の時間(new Date().getTime())から START ボタンを押した時間を引けば現在の時間が得られます。

   stopwatch_time = new Date().getTime() - press_start_time + past_moving_time;

ストップウォッチを最初にもしくは RESET ボタンを押した後はpast_moving_timeは 0 なので一旦無視してください。これについては STOP ボタンで解説します。

2) slice に関して
.slice(-2)とか.slice(-3)とかは0をパディングしてます。例えば秒を取得するとき 1 秒なら 01 を 23 秒なら 23 に変換しています。どんな秒数でも0を前にパディングしておいて後ろから 2 つ分を slice することでどんな秒数が来ても共通の処理ができます。具体的には以下の通りです。

  • 1→01→01 を取得
  • 23→023→23 を取得

3) ミリ秒、秒、分、時間の取得

stopwatch_timeはあくまでミリ秒です。こいつから秒、分、時間を取り出します。ここではstopwatch_time=123467123とします。

  • ミリ秒

stopwatch_timeは 123467.123 秒を表しています。なので下 3 桁を取得するために以下のように 1000 で割った余りを計算します。

  const time_milli_seconds = `00${stopwatch_time % 1000}`.slice(-3);

stopwatch_timeの 4,5 桁目を取得することを考えます。なのでまずは 1000 で割りMath.floorすることで123467を取得します。ストップウォッチに表示される秒数は 2 桁なので下 2 桁を取得します。60 以上になったら分に繰り上げる必要があるので123467を 60 で割ったあまりを以下のようにして取得します。

  const time_seconds = `0${Math.floor((stopwatch_time / 1000) % 60)}`.slice(-2);

こうすることで 60 秒未満の場合でも 60 秒以上 99 秒以下の場合でも同じ処理で対応できます。例えば 123467 % 60 なら 7 となり123456 % 60 なら 56みたいな感じです。

  • 分,時間

上と同様のロジックで分と時間を以下のようにして取得します。

  const time_minutes = `0${Math.floor(stopwatch_time / 1000 / 60) % 60}`.slice(
    -2
  );
  const time_hours = `0${Math.floor(stopwatch_time / 1000 / 60 / 60)}`.slice(
    -2
  );

STOP ボタン

実装方針ストップウォッチ実装の肝と言っていたpast_moving_timeについて解説します。シンプルに STOP ボタンが押されてclearIntervalをするだけだと RESTART ボタンを押したときに再び 0 秒から始まってしまいます。なぜかというと START(RESTART)ボタンを押すたびにイベントリスナーが走って以下のようにpress_start_timeが更新されるからです。

START ボタンのイベントリスナーには以下のような記述がありました。

script.js
start.addEventListener('click', () => {
  press_start_time = new Date().getTime();
  timer_id = setInterval(() => {
  stopwatch_time = new Date().getTime() - press_start_time + past_moving_time;

RESTART を押すとストップウォッチが再び 0 から始まってしまうことを避けるために定義した変数がpast_moving_timeです。

STOP ボタンのイベントリスナーのコードは以下の通りです。

script.js
stop.addEventListener('click', () => {
  clearInterval(timer_id);
  start.innerHTML = 'restart';

  press_stop_time = new Date().getTime();
  past_moving_time += press_stop_time - press_start_time;

  //STOPボタンを1度押すと非活性され、STARTボタンとRESETボタンは活性化される
  stop.disabled = true;
  start.disabled = false;
  reset.disabled = false;
});

ストップウォッチを動かしている時間の取得

past_moving_time に関してやってることはめっちゃシンプルです。STOP ボタンを押した時間から START ボタンを押した時間を引けばストップウォッチが動いていた時間を以下のように導出できます。

 past_moving_time += press_stop_time - press_start_time;

ここで+=をしているのは何度も STOP,RESTART が押されることを想定してのことです。

START ボタンのイベントリスナーの記述を見ると以下のようにpast_moving_time(ストップウォッチが動いていた時間の合計)がstopwatch_timeに加算されています。

script.js
start.addEventListener('click', () => {
  press_start_time = new Date().getTime();
  timer_id = setInterval(() => {
    stopwatch_time = new Date().getTime() - press_start_time + past_moving_time;

RESET ボタン

RESET ボタンの実装が一番簡単です。やることは以下の 2 つです。

  • ストップウォッチの停止(clearInterval)
  • 初期化(ブラウザの表示,ストップウォッチの表示をするために用いた変数)

RESET ボタンのイベントリスナーのコードは以下の通りです。

script.js
reset.addEventListener('click', () => {
  clearInterval(timer_id);

  start.innerHTML = 'start';

  //ブラウザの表示を初期化
  minutes.innerHTML = '00';
  seconds.innerHTML = '00';
  milli_seconds.innerHTML = '000';

  //変数を初期化
  stopwatch_time = 0;
  press_start_time = 0;
  press_stop_time = 0;
  past_moving_time = 0;

  //RESETボタンを押したらSTARTボタンしか押せない状態にする
  start.disabled = false;
  stop.disabled = true;
  reset.disabled = true;
});

CSS ファイル

SCSS を使っていない方もいると思うのでコンパイルした CSS をここに書いておきます。ちなみに VS Code のプラグインを使うと Webpack とかの準備をすることなく簡単に SCSS が使えるのでおすすめです。ここにプラグインに関して分かりやすく書いてありました。

style.css
style.css
body {
  margin: 0;
  padding: 0;
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
}

html {
  font-size: 20px;
}

.buttons {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
  margin-bottom: 1rem;
  margin-top: 1rem;
}

.buttons .button {
  text-transform: uppercase;
  padding: 1rem 2rem;
  margin-right: 1rem;
  color: white;
  border: none;
  cursor: pointer;
  -webkit-transition: all 0.1s ease-out;
  transition: all 0.1s ease-out;
  background: #4676d7;
  border-radius: 5px;
  font-size: 1.5rem;
  border: 2px solid transparent;
  -webkit-box-shadow: 0 0 8px gray;
          box-shadow: 0 0 8px gray;
  min-width: 225px;
}

.buttons .button:hover {
  background-color: transparent;
  color: #252020;
  border-color: #4676d7;
}

.buttons .button:disabled {
  background-color: #ccc;
}

.buttons .button:disabled:hover {
  color: white;
  border-color: transparent;
  cursor: default;
}

.display {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
  padding: 2rem;
  background-color: #d3b3ab;
}

.display .time {
  font-size: 5rem;
}

.display .time:nth-child(1)::after {
  content: ':';
}

.display .time:nth-child(2)::after {
  content: ':';
}

.display .time:nth-child(3)::after {
  content: '.';
}
/*# sourceMappingURL=style.css.map */

展望

  • START ボタンを STOP ボタンは 1 つにするべきかな?
  • React や Vue でも作りたい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReduxとReduxToolkitを使用してReact内でデータを管理する

はじめに

前回のReactを使用してWeb画面を作成するでは、Reactの簡単なサンプルと共に説明をまとめました。次はデータを持ちまわすためにReduxとReduxToolkitの簡単なサンプルと説明をまとめようと思います。
最低限の簡単なサンプルなのでアクションは別ファイルにするべきであったり、といったベストプラクティス的なものは他のサイトで調べてください。

環境

  • node.js: v12.18.2
  • webpack: 4.44.1
  • React: 16.13.1
  • Redux: 7.2.1

環境作成

前回のReactを使用してWeb画面を作成するの続きとなります。
環境構築などはそちらを見てください。

Redux

Reactのコンポーネント間でデータを共有するためにReduxというライブラリを使用します。

Reduxのインストール

ReduxのライブラリとReduxをシンプルに記載するためのtoolkitをインストールします。

npm install react-redux
npm install @reduxjs/toolkit

シンプルなRedux

Reduxのソースの作成

フォルダの作成

Reduxのソースをstore、slice、コンポーネントに分けて作成するので、コンポーネント以外のフォルダを作成してください。コンポーネントは前回作成したsrcの下に入れます。

project_root
├─src    // reactのJavaScriptファイルやCSSファイルを格納
├─store  // redux toolkitのstore(reduxのstoreをまとめたもの)ファイルを格納
  ├─slice    // redux toolkitのslice(reduxのactionとreducerをまとめたもの)

sliceファイルの作成

内部的に保持する情報と処理をまとめたものをsliceファイルとして作成しています。
今回は単純にWeb画面と文字のやり取りをするため保持するデータは"mess"、処理は"hello"をデータに置き換える処理としています。

messageSlice.js
import { createSlice } from '@reduxjs/toolkit';
import axios from 'axios';

export const messageSlice = createSlice({
  // slice名
  name: 'message',
  // 内部で保持するデータ(キー:mess, 初期値:メッセージ)
  initialState: {
      "mess": "メッセージ"
  },
  // 内部のデータにアクセスするための処理(処理名:sayhello)
  reducers: {
    sayhello: state => {
      state.mess = "hello";
    }
  },
});

// 外からインポートするためにactionとreducerをエクスポートする
export const { sayhello } = messageSlice.actions;
export default messageSlice.reducer;

storeファイルの作成

先ほど作成したsliceのreducerをstoreに登録することで各コンポーネントで情報を共有できるようにします。

store.js
import { configureStore } from '@reduxjs/toolkit';
import messageReducer from './slice/messageSlice';

export default configureStore({
  reducer: {
    message: messageReducer,
  },
});

storeをコンポーネントに適用させる

コンポーネント間で情報をやり取りするために先ほど作成したstoreをreactのrenderに登録します。

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

// redux用のインポート
import { Provider } from 'react-redux'
import store from '../store/store'

ReactDOM.render(
  // インポートしたstoreを登録する
  <Provider store={store}>
    <App />,
  </Provider>,
  document.getElementById('app')
);

slice経由でstoreを使用する

コンポーネントで情報を処理するためにsliceのaction経由でstoreを操作します。

App.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { sayhello } from '../store/slice/messageSlice';

export function Message() {
  // store内の値を取得
  const message = useSelector(state => state.message.mess);
  // actionを操作するための関数取得
  const dispatch = useDispatch();

  return (
    <div>
      <div>
        {/* Sliceで定義したactionをdispatch経由で呼び出す */}
        <button aria-label="hello" onClick={() => dispatch(sayhello())}>
          こんにちは
        </button>
        {/* 上で呼び出したmessageを表示する */}
        <span>{message}</span>
      </div>
    </div>
  );
}

最終的なフォルダ構成

project_root
├─dict
├─public
|  Ⅼ-index.html
├─src    
|  ├─App.js
|  Ⅼ─index.js
├─store  
|  ├─slice  
|  | Ⅼ─messageSlice.js
|  Ⅼ-store.js
├─.babelrc
├─package.json
Ⅼ─webpack.config.js

Reactの実行

前回同様にReactを開発用のサーバで起動してブラウザからアクセスしてください。

"./node_modules/.bin/webpack-dev-server"

表示されたWeb画面にこんにちはというボタンがあると思うため、それをクリックすると隣の文字がhelloに変わります。
内部の処理としては、ザックリ言うと以下のようなイメージになります。
画面描画時にApp.js内でuseSelectorを使用することにより、messageSlice.jsで定義したmess変数を呼び出しています。
その呼び出した変数を<span>{message}</span>と紐づけて変数が変わったら自動的に変わるように使用しています。
ボタンを押したらmessageSlice.jsで定義したsayhelloを呼び出してmess変数を更新して、再描画しています。

画面から受け取るRedux

先ほどはactionの中で定義した値に更新していましたが、今度はWeb画面に入力した内容を使用してstoreを更新します。

Reduxのソースの作成

先ほどのファイルに処理を追加して機能を実装します。

sliceファイルの作成

messageSlice.js
import { createSlice } from '@reduxjs/toolkit';

export const messageSlice = createSlice({
  name: 'message',
  ~~~ 上と同じ ~~~

  reducers: {
  ~~~ 上と同じ ~~~
  // この処理を追加します。
    sayAmount: (state, action) => {
      state.mess = action.payload;
    },
  },
});

// 追加したsayAmountをエクスポートできるようにする
export const { sayhello, sayAmount} = messageSlice.actions;

export default messageSlice.reducer;

storeファイルの作成

storeファイルは、上のreducerをまとめて登録しているので変更なしです。

storeをコンポーネントに適用させる

storeファイルは、上のreducerをまとめて登録しているので変更なしです。

slice経由でstoreを使用する

内容としては。ほとんど先ほどのものと変わらないです。
8行目のところでuseStateを使用して2つの関数を生成していますが、ザックリ言うとクラス内のstateの宣言などを不要にする物になります。

Message.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { sayhello, sayAmount } from '../store/slice/messageSlice';

export function Message() {
  const message = useSelector(state => state.message.mess);
  const dispatch = useDispatch();
  const [messsageAmount, setMesssageAmount] = useState('2');
  ~~~ 上と同じ ~~~
        {/* テキストボックスとボタンにインポートしたものを適用する  */}
        <input aria-label="set amount" value={messsageAmount} onChange={e => setMesssageAmount(e.target.value)} />
        <button onClick={() => dispatch(sayAmount(messsageAmount))}>
          テキスト変更
        </button>
      </div>
    </div>
  );
}

Reactの実行

上と同様にReactを開発用のサーバで起動してブラウザからアクセスしてください。
今回追加したテキストボックスとボタンが追加されています。テキストボックスに値を入れてボタンをクリックすると表示されているテキストが変更されます。
基本的に内容としてはほぼ表示のみと変わりません。

終わりに

axiosとの連携を書こうと考えていましたが、Reduxの説明が予想以上に長くなったので今回はここまでにします。次回以降にaxiosのサンプルと簡単な説明を書いていこうと思います。axiosのサンプルと説明はReactのRedux内でaxiosを使用した通信をするにまとめました。
今回は簡単な例なのでメリットがわからないと思いますが、Web画面や保持する情報が増えたら明確にメリットがわかるようになると思います。

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

JavaScriptで24ビット ビットマップフォーマットをエンコードする

まえがき

以下の解説はいたしません。

  • RGB
  • ビットマップフォーマットのヘッダーレイアウト
  • ビットマップフォーマットの画像データレイアウト
  • リトルエンディアンとビッグエンディアン

背景

node-canvasをつかってSVGファイルをビットマップファイルに変換する - QiitaでCanvasのImageDataから、32ビット ビットマップフォーマットのエンコードへの変換を実装しました。

32ビット ビットマップの問題

32ビット ビットマップフォーマット では、1つの画素につき4バイト、32ビット幅の領域で色情報を表現します。4バイトの内訳は、「RGBAの4つの情報にそれぞれ1バイトずつ」と思いきや、4番目はAlphaではなく、Reserved領域です。予約されていますが、画像の情報として、つかわれていません。予約した当初は、Alphaにつかう予定があったかもしれません。現在はただの空き領域です。

100 x 100の画像があった場合に、
32ビット ビットマップフォーマットだと、320,000ビット、40,000バイト、39KBです。
Reservedを使わなければ、240,000ビット、30,000バイト、29KBで済みます。
24ビット ビットマップフォーマットが、まさにRGBに1バイトずつつかい、予約領域をつかわないファイルフォーマットです。

目的

というわけで、出力ファイルサイズを節約するために、32ビットマップフォーマットを書き出すプログラムを、24ビット ビットマップフォーマットを書き出すプログラムに改良します。

24ビット ビットマップをエンコードするための課題

24ビット単位の書き出し

32ビット ビットマップフォーマットでは、4バイトの色情報はBGRA(正確にはAlphaではなくReservedですが、Redと見分けやすいので、便宜上Aと書きます)の順番で並べます。RGBの3バイトの情報(一番上位のバイトは初期値の0が入っていることを期待しています)を用意し、32ビットリトルエンディアンで書き込むと、バイト単位の順番が上手いこといれかわって、BGRAになります。次のイメージです。

const bodyData = new Uint32Array(data.length);

// 中略

bodyData[j] = (data[i + 0] << 16) | // Red
              (data[i + 1] << 8) | // Green
               data[i + 2]; // Blue

Uint32Arrayは、Intel CPUやArm CPUの上で動く、大抵の処理系ではリトルエンディアンで書き込まれます。

24ビット情報はこの方法では書き込めません。JavaScriptのTypedArrayにはUint24Arrayはありません。DataViewをつかって1バイトずつ書きます。
次のイメージです。

const buffer = new DataView(new ArrayBuffer(fileSize));

// 中略

buffer.setUint8(j, data[i + 2]); // Blue
buffer.setUint8(j + 1, data[i + 1]); // Green
buffer.setUint8(j + 2, data[i + 0]); // Red

Uint32Arrayをつかった方法と見比べて見ましょう。RGBの順で書いていたのを、BGRの順番に変更しています。Uint32Arrayが自動的にリトルエンディアンに入れ替えていた代わりに、手動で順番を入れ替えています。

単に1バイトずつ書き込むのであればUint8Arrayを使うこともできます。Uint8Arrayを使わずに、DataViewをつかったのは次の理由です。
ビットマップフォーマットのヘッダは2バイトと4バイトの領域があり、その書き込みにすでにDataViewをつかっています。2種類のAPIをつかうより、1つにまとめた方がプログラムは読みやすいです。Uint8ArrayかDataViewのどちらかにまとめます。
ヘッダの書き込みも、Uint8Arrayに統一すると、2バイト、4バイトの情報にもリトルエンディアンへの順番変更をする必要があります。DataViewには任意のバイトサイズをエンディアンを指定して書き込むAPIがあります。DataViewに統一しました。

行のパディング

ビットマップフォーマットの画像データは1行の画像データを4バイトで区切ります。

たとえば、横幅が5ピクセルの画像の場合を考えます。
24ビット ビットマップフォーマットでは、1行目の画像データは5掛ける3バイト、15バイトです。
2行目の情報は、16バイト目からは始めず、1バイト開けて、17バイト目から書きます。
行ごとに4バイトの倍数に合わせるために、1バイトパディングします。

32ビット ビットマップフォーマットでは、1行目の画像データが、5掛ける4バイト、20バイトです。2行目の情報は、つづけて、21バイト目から書きます。実は、32ビットの場合は、1ピクセルの情報が4バイトなので、画像の幅がいくつでも必ず1行の画像データは必ず4バイトの倍数になります。
行のパディングを意識する必要はありませんでした。そのため32ビット ビットマップフォーマットを書き出すプログラムではパディングの計算をしていませんでした。

24ビット ビットマップフォーマットでは、1行ごとに開けるバイト数を計算する必要があります。
次の式で求められます。

4 - (width * 24) % 4

画像データの1行の幅はひとつのファイルの途中で変わることはありません。ファイルごとに一回計算すれば十分です。

画像データサイズの計算

前述の行のパディングがあるため、画像のデータサイズはピクセル数 x 3バイトにはなりません。
行のパディングを考慮すると1行のバイト数は次の式で求められます。

width * 3 + linePadding

これに行数を掛けると、画像データのサイズが得られます。

(width * 3 + linePadding) * height

ファイルサイズの計算

画像データサイズに行のパディングが入るため、ファイルサイズにも考慮が必要です。

headerSize + (width * 3 + linePadding) * height

ここではヘッダーサイズは54バイト固定とします。
ビットマップフォーマットには情報ヘッダーの種類にはいくつか種類がありますが、Windowsビットマップファイルフォーマット用のINFOタイプを使います。

実装

以上の課題を踏まえた実装が次です。

// canvasImageDataをビットマップフォーマットに変換します。
// ビットマップフォーマットの仕様は下記サイトに準拠します。
// http://www.umekkii.jp/data/computer/file_format/bitmap.cgi
// https://www.ruche-home.net/program/bmp/struct

const headerSize = 54; // ファイルヘッダ(14byte) + ファイル情報ヘッダ(40byte)= 54byte 固定

module.exports = class Canvas2Bitmap {
  constructor(canvasImageData) {
    this._depth = 3; // 色ビット数(バイト単位)
    this._canvasImageData = canvasImageData;
    this._buffer = new DataView(new ArrayBuffer(this._fileSize));
  }

  get _width() {
    return this._canvasImageData.width;
  }

  get _height() {
    return this._canvasImageData.height;
  }

  get _linePadding() {
    return 4 - (this._width * this._depth) % 4;
  }

  get _lineDataSize() {
    return this._width * this._depth + this._linePadding
  }

  get _bodySize() {
    return this._lineDataSize * this._height;
  }

  get _fileSize() {
    return headerSize + this._bodySize;
  }

  _fillFileHeader() {
    // bfType ファイルタイプ BM固定
    this._buffer.setUint8(0x0, "BM".charCodeAt(0));
    this._buffer.setUint8(1, "BM".charCodeAt(1));
    this._buffer.setUint32(2, this._fileSize, true); // bfSize ファイルサイズ
    this._buffer.setUint16(6, 0); // bfReserved1 予約領域 0固定
    this._buffer.setUint16(8, 0); // bfReserved2 予約領域 0固定
    this._buffer.setUint32(10, headerSize, true); // bfOffBits ファイルの先頭から画像データまでのオフセット[byte]
  }

  // 情報ヘッダ INFOタイプ
  _fillImageHeader() {
    this._buffer.setUint32(14, 40, true); // biSize 情報ヘッダサイズ INFOタイプでは 40
    this._buffer.setUint32(18, this._width, true); // biWidth 画像の幅[ピクセル]
    this._buffer.setUint32(22, this._height, true); // biHeight 画像の高さ[ピクセル]
    this._buffer.setUint16(26, 1, true); // biPlanes プレーン数 1固定
    this._buffer.setUint16(28, this._depth * 8, true); // biBitCount 色ビット数[bit] 1, 4, 8, 16, 24, 32
    this._buffer.setUint32(30, 0, true); // biCompression 圧縮形式 0, 1, 2, 3
    this._buffer.setUint32(34, this._bodySize, true); // biSizeImage 画像データサイズ[byte]
    this._buffer.setUint32(38, 0, true); // biXPixPerMeter 水平解像度[dot/m] 0で良さそう
    this._buffer.setUint32(42, 0, true); // biYPixPerMeter 垂直解像度[dot/m] 0で良さそう
    this._buffer.setUint32(46, 0, true); // bitClrUsed 格納パレット数[使用色数]
    this._buffer.setUint32(50, 0, true); // bitClrImportant 重要色数
  }

  _fillBody() {
    const data = this._canvasImageData.data;

    // ある行を左から右に進んで行く
    for (var x = 0; x < this._width; x++) {
      // 上から下に行を進んで行く
      for (var y = 0; y < this._height; y++) {
        // canvasのimageDataは1バイトごとにRGBAが分かれている。
        // 画素単位の4バイトずつ進みます。
        const i = (y * this._width + x) * 4;

        // ビットマップは左下から右上に記録されているので、下から詰めていく
        const j = headerSize +
          this._lineDataSize * (this._height - y - 1) +
          x * this._depth;

        // 24bitビットマップ
        // 1画素あたり24bit(3byte)で、Blue(8bit)、Green(8bit)、Red(8bit)。
        // 137, 41, 69, 255だとしたら?
        // 0x45, 0x29, 0x89
        this._buffer.setUint8(j, data[i + 2]); // Blue
        this._buffer.setUint8(j + 1, data[i + 1]); // Green
        this._buffer.setUint8(j + 2, data[i + 0]); // Red
      }
    }
  }

  // WebでもNodeでも扱いやすい、Uint8Arrayを返します。
  get buffer() {
    this._fillFileHeader();
    this._fillImageHeader();
    this._fillBody();
    return new Uint8Array(this._buffer.buffer);
  }
};

次のように使います。

const { createCanvas, loadImage } = require("canvas");
const Canvas2Bitmap = require("./Canvas2Bitmap")
const fs = require("fs");

function getCanvasImageData(image) {
  const canvas = createCanvas(image.width, image.height);
  const ctx = canvas.getContext("2d");
  ctx.drawImage(image, 0, 0);
  return ctx.getImageData(0, 0, image.width, image.height);
}

!(async function () {
  const image = await loadImage(process.argv[2]);
  const canvasImageData = getCanvasImageData(image);
  const bitmap = new Canvas2Bitmap(canvasImageData)
  const stream = fs.createWriteStream('out.bmp');
  stream.write(bitmap.buffer);
  stream.end();
})();

関連

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

Javascript基礎文法

はじめに

javascriptの基礎が定着するようにここでアウトプットしておきます。
至らない点などあればアドバイスいていただけると幸いです!!
じゅぶん自身最近初めてばかりなのでES6のありがたみなどは全く理解していないのでそのためにもまず基礎の基礎をここで書いておこうかなと思います!!

目次

1.変数と定数宣言
 ・var
 ・let
 ・const
2.関数宣言
 ・function宣言
 ・関数式
 ・アロー関数
3.終わりに

変数と定数宣言

ES6以前では変数の宣言でvarしか使えなかった!!

var

var 変数名 = '初期値';
変数名 = '再代入';
var 変数名 = '再宣言';

これら全てがvarでは可能
また関数内のどこからでも参照可能!!
そのためvarだとブロックスコープの変数が使えなかった!!ブロック{}外からも参照できてしまうため!!

let

let 変数名 = '初期値';
変数名 = '再代入';
let 変数名 = '再宣言できない';

再代入はできるが再宣言はできない
ブロック内{}でしか呼び出せない!!(ブロックスコープ)
varと比べてスコープが狭くなる

const  

const 変数名 = '初期値'
変数名 = '再代入できない'
const 変数名 = '際宣言できない'

定数になる!!
ブロックスコープ(let)と同じ!!
中身が配列であれば、再代入できる!!

管理しやすくするためスコープはできるだけ狭くしたほうがいい!!
そのためブロックスコープのconstとletを使う
constでも配列であれば値は変更(再代入できる)ので配列ではなく再代入が必要なときはletを使う!!
できるだけconstを使うことがおすすめ

関数宣言

function宣言

function 関数名 () {処理}

関数式

const 変数名 = function(){処理}  //無名関数
const 変数名 = fucntion 関数名(){処理}  //名前付き関数

ここで変数として宣言しておくことで、そのまま呼び出さず、代入して呼び出す方式
関数宣言と違うのは、その関数よりも前で呼び出すと、文法エラーになる!!

アロー関数

const 関数名 = () => {処理}

関数式でのfunctionを省き、アロー(矢印)で代替したものになる!!
内容としては関数式と同じ感じ
この宣言方法が公式では推奨!!

関数宣言と関数式(アロー関数)の違い

functionオブジェクトの生成されるタイミングが違う
関数宣言は宣言を含むスコープが実行されるときにfunctionオブジェクトが生成される
関数式はその式が実行されるときにfunctionオブジェクトが生成される!!
このことでfunctionオブジェクトが重ければパフォーマンスの違いは出てくると考えられる!!

ここについてはきちんとまだ理解していないです!!こちらから参照させていただきました。
公式推奨の通りconstとアロー関数を中心にコードを書いていこうと思います!!

終わりに

ここでは、javascriptの基礎の基礎を書かせていただきました。
初心者が自分用に書いておりますので何かの間違いがあるかもしれません。その時はアドバイスしていただけると幸いです!!

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

railsでページをリロードしないとJSが機能しない件

タイトル通りJSのイベントが起きるためにリロードが必要な状況を改善したい。

検索すると以下の参考記事があったので、素直にgemfileとapprication.jsを変更し対応しようとしたがうまくいかない。
https://qiita.com/Terunaga/items/19d4f49f3abd3316f098

別の記事で以下のようにjsのリロードが必要なページに追記することで解決できた。
http://taremimi.hatenablog.jp/entry/2018/06/06/085030

<body data-turbolinks="false">

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

Express + MongoDBで作成したAPIサーバーをJestでテストする

はじめに

テストを書くのは好きですか?

アプリケーションを作成するうえで、テストは必ず書かなきゃいけないものですが、テストの書き方まで丁寧に記載されている書籍やサイトって少ないように感じます。

今回はExpress + MongoDBで作成したAPIサーバーをJestでテストします。

まずはモジュールごとに依存関係を切り離した単体テストを作成します。
Jestの強力なモック機能を活用することができます。

その後supertestを用いた結合テストまで作成します。
supertestは擬似的なHTTPリクエストを送ることができます。

テスト対象のアプリケーション

予め作成しておいた以下のコードをテストします。
機能としては簡単に、/api/users/:usernameGETで叩くと指定したユーザーネームのユーザーを取得でき、/api/usersPOSTで叩くと、ユーザーを作成できるという最小限のものです。

すべてのコードはここから確認することができます。
https://github.com/azukiazusa1/express-test

MVCモデルにのっとり、大きくわけてルーティングコントローラーモデルで構成されています。

src
  ├ controllers
    ├ userController.ts
  ├ middleware
    ├ error.ts
  ├ models
    ├ userModel.ts
  ├ routes
    ├ index.ts
    ├ userRoutes.ts
  ├ index.ts
package.json
package-lock.json
tsconfid.json

以下、srcフォルダの中身です、

index.ts

src/index.ts
import Express from 'express'
import bodyParser from 'body-parser'
import mongoose from 'mongoose'
import router from './routes'
import errorMiddleware from './middleware/error'

const app = Express()
const port = 3000

// dbに接続
mongoose.connect('mongodb://localhost:27017/express-test', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useCreateIndex: true
})

mongoose.Promise = global.Promise

// postリクエストを受け取るための設定
app.use(
  bodyParser.urlencoded({
    extended: true
  })
)
app.use(bodyParser.json())

// /api以下にルートを作成
app.use('/api', router)

// エラーハンドリング
app.use('/', errorMiddleware.notFound)
app.use(errorMiddleware.errorHandler)

// サーバースタート
app.listen(port, () => {
  console.log('server start')
})

export default app

routes/index.ts

src/routes/index.ts
import Express from 'express'
import userRoutes from './userRoutes'

// すべてのルーティングをここにまとめます。
const router = Express.Router()

router.use('/users', userRoutes)

export default router

routes/userRoute.ts

src/routes/userRoute.ts
import Express from 'express'
import usersController from '../controllers/usersController'

const router = Express.Router()

router.get('/:username`, userController.show)
router.post('/', usersController.create)

export default router

controllers/userController.ts

src/controllers/userController.ts
// controllerはModelとリクエストを仲介します。
import Express from 'express'
import User from '../Models/user'

export default {
 // ユーザーを一人返す
  show: async (
    req: Express.Request,
    res: Express.Response,
    next: Express.NextFunction
  ) => {
    try {
      const username: string = req.params.username
      const user = await User.findOne().findByUserName(username)
      res.status(200).json({ user })
    } catch (e) {
      next(e)
    }
  },
 // ユーザーを作成する
  create: async (
    req: Express.Request,
    res: Express.Response,
    next: Express.NextFunction
  ) => {
    try {
      const user = await User.create(req.body)
      res.status(201).json({ user })
    } catch (e) {
      next(e)
    }
  }
}

models/user.ts

src/models/user.ts
import mongoose, { Schema, Document, Model, DocumentQuery } from 'mongoose'
import { User } from '../types/user'

export interface UserDoc extends Document, User {
  fullName?: string
}


// スキーマを定義
const userSchema: Schema = new Schema(
  {
    username: {
      type: String,
      required: true,
      unique: true
    },
    fristName: {
      type: String
    },
    lastName: {
      type: String
    },
    gender: {
      type: String,
      required: true,
      enum: ['male', 'female']
    },
    age: {
      type: Number,
      min: 0,
      max: 100
    }
  },
  {
    timestamps: true
  }
)

// バーチャルフィールド
userSchema.virtual('fullName').get(function(this: User) {
  return `${this.firstName} ${this.lastName}`
})

// クエリヘルパー
const queryHelpers = {
  findByUserName(this: DocumentQuery<any, User>, username: string) {
    return this.findOne({ username })
  }
}
userSchema.query = queryHelpers

interface UserModel extends Model<User, typeof queryHelpers> {}

export default mongoose.model<User, UserModel>('User', userSchema)

テストを記述する

それでは、これからテストを記述していきましょう。
幸いなことに、今回テストするアプリケーションは、ルーティングコントローラーモデルに切り離されて構築されているので、それぞれに単体レベルでテストを書くことができます。

それぞれのモジュールが依存するモジュールをモック化することで、影響範囲を小さくしてテストをすることができます。

そして、最終的にはモジュールを結合して擬似的なHTTPリクエストを送る、結合テストまで記述していきます。

Jestのテスト環境の構築

今回は、Jestというテストフレームワークを使ってテストを記述していきます。

まずはJestをインストールしましょう。

npm i -D jest @types/jest

package.json

package.jsonのscriptsをJestを使用するように書き換えます。

package.json
{
  "name": "express-test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
--  "test": "echo \"Error: no test specified\" && exit 1",
++  "test": "jest",
    "serve": "ts-node-dev src/index.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/mongoose": "^5.7.34",
    "express": "^4.17.1",
    "mongoose": "^5.9.27"
  },
  "devDependencies": {
    "@types/express": "^4.17.7",
    "@types/node": "^14.0.27",
    "ts-node-dev": "^1.0.0-pre.56",
    "typescript": "^3.9.7"
  }
}

tsconfig.json

Visual Studio Codeに怒られないように、tsconfig.jsonにJestの型定義を追加します。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "moduleResolu``tion": "node",
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true
++  "types": ["@types/jest"]
  }
}

babel.config.js

Babelを使用するための設定を追加します。Babelを使用することで、テストファイル中でもimport/export構文が使えたり、ECMAScriptの新しい記法を使うことができます。

まずは必要なモジュールをインストールします。

npm i -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript

プロジェクトルートに、babel.config.jsonを追加します。

babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {targets: {node: 'current'}}],
    '@babel/preset-typescript',
  ],
};

jest.config.js

Jestではデフォルトのテスト環境はブラウザになるので、Node.jsの環境でテストするように設定を追加します。

プロジェクトルートにjest.config.jsを追加します。

jest.config.js
module.exports = {
  testEnvironment: 'node'
}

テストのテスト

テストのテストを書いて確認してみましょう。testフォルダを作成し、その中にindex.spec.tsファイルを作成し、簡単なテストを記述します。

test/index.spec.ts
describe('simple test', () => {
  test('1 === 1', () => {
    expect(1).toBe(1)
  })

  test('1 === 2', () => {
    expect(1).toBe(2)
  })
})

テストはnpm testで実行します。うまくいけば、テストは一つは成功し、もう一つは失敗します。

npm test

> express-test@1.0.0 test /express-test
> jest

 FAIL  test/index.spec.ts
  simple test
    ✓ 1 === 1 (3 ms)
    ✕ 1 === 2 (6 ms)

  ● simple test › 1 === 2

    expect(received).toBe(expected) // Object.is equality

    Expected: 2
    Received: 1

       5 | 
       6 |   test('1 === 2', () => {
    >  7 |     expect(1).toBe(2)
         |               ^
       8 |   })
       9 | })
      10 | 

      at Object.<anonymous> (test/index.spec.ts:7:15)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        2.669 s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

コントローラーのテスト

まずは、コントローラーからテストを記述していきます。
コントローラーの責務から、次の観点に基づいてテストを記述します。

  • リクエストを受け取った結果正しいレスポンスを返すか
  • 正しいステータスコードを返すか
  • エラーハンドリングが正しく行われているか

テスト対象をコントローラーに絞ってテストを書きたいですが、コントローラーはExpress(レスポンスとリクエストの処理)とモデル(データベースの処理)に依存しています。
この2つのモジュールをモック化しましょう。

test/controller/userController.spec.tsに記述していきます。

Expressをモック化する

Expressをモック化するために、sinon-express-mockというモジュールをインストールします。
これは、Expressのリクエストとレスポンスメソッドをを簡単にスパイ化してくれます。

npm i -D sinon-express-mock @types/sinon-express-mock sinon

使い方は、以下のとおりです。

mockReqにオブジェクトを渡して生成すると、モックリクエストを渡すことができます。
mockReq()mockRes()で生成されたのは、スパイメソッドです。スパイ化されたメソッドは引数、戻り値、thisの値、およびすべての呼び出しに対してスローされた例外(存在する場合)を記録します。

よって、resオブジェクトが何回呼び出されたか、どのような引数で呼び出されたかなどをテストすることができます。この例では、res.status()がステータスコード201で呼び出されていることをテストしています。

test/controllers/userController.spec.ts
import { mockReq, mockRes } from 'sinon-express-mock'
import { User } from '../../src/types/user'
import usersController from '../../src/controllers/usersController'

interface Request {
  body: User
}

describe('src/cotrollers/userController', () => {
  test('create', async () => {
    const request: Request = {
      body: {
        username: 'username',
        gender: 'male',
        age: 22
      }
    }
    const req = mockReq(request)
    const res = mockRes()
    const next = jest.fn()

    await usersController.create(req, res, next)
    expect(res.status.calledWith(201)).toBeTruthy()
  })
})

モデルをモック化する

次に、モデルをモック化します。
モデルをうまくモック化するためには、Staticsやクエリヘルパーなどを活用して、詳細なクエリはモデルに閉じ込めるのがよいです。

Jestのモック関数を利用する

モデルをモック化するために、Jestのモック関数を利用します。jest.mock()でモジュールを指定すると、モジュールの依存をモック関数で上書きすることができます。

test/controllers/userController.spec.ts
jest.mock('../../src/Models/user', () => ({
  create: jest.fn((user: User) => {
    const _id = '12345'
    return Promise.resolve({ _id, ...user })
  })
}))

モデルをモック化し、本来エクスポートされたUserモデルが持っているcreateメソッドはモック関数として受け取ったオブジェクトに_idプロジェクトを足して返すという単純なものになっています。

これで、他のモジュールとの依存を切り離し、テストを書くことができます。

test/controllers/userController.spec.ts
import { mockReq, mockRes } from 'sinon-express-mock'
import { User } from '../../src/types/user'
import usersController from '../../src/controllers/usersController'
import { create } from 'domain'

interface Request {
  body: User
}

jest.mock('../../src/Models/user', () => ({
  create: jest.fn((user: User) => {
    const _id = '12345'
    return Promise.resolve({ _id, ...user })
  })
}))

describe('src/cotrollers/userController', () => {
  test('create', async () => {
    const testUser: User = {
      username: 'username',
      gender: 'male',
      age: 22
    }
    const request: Request = {
      body: testUser
    }
    const req = mockReq(request)
    const res = mockRes()
    const next = jest.fn()

    await usersController.create(req, res, next)
    expect(res.status.calledWith(201)).toBeTruthy()

    const { user } = res.json.getCall(0).args[0]
    expect(user.username).toEqual(testUser.username)
    expect(user.gender).toEqual(testUser.gender)
    expect(user.age).toEqual(testUser.age)
  })
})

エラーハンドリングをテストする

モデルをモック化したので、意図的にデータベースのエラーを発生してエラーハンドリング処理をテストすることができます。

変数mockErrorを宣言し、この値がtrueのときにはモック関数がエラーを返すようにします。

test/controllers/userController.spec.ts
let mockError = false

jest.mock('../../src/Models/user', () => ({
  create: jest.fn((user: User) => {
    if (mockError) {
      return Promise.reject('Mock Error!')
    }
    const _id = '12345'
    return Promise.resolve({ _id, ...user })
  })
}))

Expressのエラーハンドリングでは、エラーが発生した場合next()関数にエラーオブジェクトを渡してエラーハンドラー関数に処理を委任することによって行われています。

next()関数が呼ばれているかどうかでエラーハンドリング処理が行われているかどうかテストしましょう。

test/controllers/userController.spec.ts
describe('異常系', () => {
  test('エラーが発生したらnext()が呼ばれる', async () => {
    mockError = true

    const req = mockReq(request)
    const res = mockRes()
    const next = jest.fn()

    await usersController.create(req, res, next)

    expect(next).toBeCalledWith('Mock Error!')
  })
})

モデルのテスト

続いてモデルをテストします。以下の観点でテストを行います。

  • データの保存、削除、更新などが正しく行われているか
  • スキーマに対してバリデーションが正しく働いているか
  • クエリヘルパーやバーチャルフィールドなど、自作したメソッドが正しく動作するか

テスト用のデータベースを用意する

モデルのテストをするためには、データベースと接続する必要があります。
とはいえ実際の環境のデータベースをテスト用のデータで汚したくありませんし、テストの実行に時間がかかるのも嫌でしょう。

そこで、今回は@shelf/jest-mongodbを利用して、メモリサーバーのMongoDBを使用します。

メモリサーバーのセットアップ

インストール

まずはモジュールをインストールします。

npm i -D @shelf/jest-mongodb
jest.config.js

次に、jest.config.jsに以下を追記します。

jest.config.js
module.exports = {
++ preset: '@shelf/jest-mongodb',
// testEnvironmentは競合するので削除
-- testEnvironment: 'node'
}
jest-mongodb-config.js

jest-mongodb-config.jsを作成して、以下の設定を記述します。
設定可能なすべてのオプションは、こちらを参照してください。

jest-mongodb-config.js
module.exports = {
  mongodbMemoryServerOptions: {
    instance: {
      dbName: 'jest'
    },
    binary: {
      version: '5.9.25', // Version of MongoDB
      skipMD5: true
    },
    autoStart: false
  }
}
.gitignore

テストを実行するたびに、globalConfig.jsonというファイルが吐き出されるので、.gigignoreに追記しておくと良いでしょう。

.gitignore
++ globalConfig.json

これで設定は完了です。test/Models/user.spec.tsにテストを記述していきます。

test/Models/user.spec.ts
import mongoose from 'mongoose'
import User from '../../src/Models/user'
import { User as UserType } from '../../src/types/user'

// テストデータ
const users: UserType[] = [
  {
    username: 'user1',
    firstName: 'aaa',
    lastName: 'bbb',
    gender: 'male',
    age: 22
  },
  {
    username: 'user2',
    firstName: 'ccc',
    lastName: 'ddd',
    gender: 'male',
    age: 30
  },
  {
    username: 'user3',
    firstName: 'eee',
    lastName: 'fff',
    gender: 'female',
    age: 34
  }
]

describe('src/models/user', () => {
  // データベースに接続
  beforeAll(async () => {
    mongoose.Promise = global.Promise
    await mongoose.connect((global as any).__MONGO_URI__, {
      useNewUrlParser: true,
      useCreateIndex: true,
      useUnifiedTopology: true
    })
  })

  // テストデータをテスト毎に挿入
  beforeEach(async () => {
    await User.deleteMany({})
    await User.collection.insertMany(users)
  })

  // 接続を閉じる
  afterAll(() => {
    mongoose.connection.close()
  })

  describe('クエリヘルパー', () => {
    describe('findOrCreate',() => {
      test('指定したusernameのユーザーが取得できる', async () => {
        const result = await User.findOne().findByUserName('user1')
        expect(result?.username).toEqual('user1')
      })
    })
  })
})

ポイントは、beforeAllbeforeEachafterAllでデータベースに関する準備を行っているところです。順番に見ていきましょう。

beforeAll

beforeAllは、describeブロックの中でテストを実施する前に一度だけ呼び出されます。後述のbeforeEachよりも先に呼び出されます。

ここではメモリサーバーへの接続を行っています。
通常と異なる点として、mongooseの接続先に(global as any).__MONGO_URI__を指定しています。

__MONGO_URI__はメモリサーバーの接続先を表しています。

beforeEach

beforeEachdescribeブロックの中ですべてのテストごとに実施されます。
ここで一度すべてのデータを削除してからテストデータを挿入することによって、テスト間の依存が発生しないようにしています。

collection.insertMany()create()よりも早く一括でデータを挿入することができますが、バリデーションは実施されないので注意が必要です。

afterAll

afterAlldescribeブロックの中で最後に一度だけ呼び出されます。

ここでデータベースとの接続を切っておかないとテストが正常に終了しないので忘れずにここの処理を書いておきましょう。

バリデーションテスト

バリデーションエラーが発生した場合、例外をスローするので、そのことをテストで確認します。

例外がスローされたかどうかはexpect().rejects.toThrow()で確認します。

test/Models/user.spec.ts
import mongoose, {Error} from 'mongoose'
import User from '../../src/Models/user'
import { User as UserType } from '../../src/types/user'
const { ValidationError } = Error

// 中略
describe('バリデーション', () => {
    describe('username', () => {
      test('usernameはuniqueでなけれなばらない', async () => {
        const invalidUser: UserType = {
          username: 'user1',
          firstName: 'firstName',
          lastName: 'lastName',
          gender: 'female',
          age: 18
        }
        await expect(User.create(invalidUser)).rejects.toThrow()
      })

      test('usernameは必須項目でなけれなばらない', async () => {
        const invalidUser: UserType = {
          username: '',
          firstName: 'firstName',
          lastName: 'lastName',
          gender: 'female',
          age: 18
        }
        await expect(User.create(invalidUser)).rejects.toThrow()
      })
    })

バーチャルフィールドテスト

このテストは特に難しいところもないでしょう。

test/Models/user.spec.ts
describe('バーチャルフィールド', () => {
    describe('fullName', () => {
      test('firstNameとLastNameを足して返す', async () => {
        const result = await User.findOne().findByUserName('user1')
        expect(result!.fullName).toEqual('aaa bbb')
      })
    })
  })

ルーティングテスト

最後に、ルーティングのテストを記述します。
このテストは比較的簡単です。

Express.Router()userControllerをモック化して、各ルートに対応するコントローラーが割り当てられているか確認します。

test/routes/userRoutesに記述つします。

import userRoutes from '../../src/routes/userRoutes'
import usersController from '../../src/controllers/usersController'

jest.mock('Express', () => ({
  Router: () => ({
    get: jest.fn(),
    post: jest.fn()
  })
}))
jest.mock('../../src/controllers/usersController')

describe('src/routes/userRoutes', () => {
  test('get /api/users/:usernameには、showアクションが呼ばれる', () => {
    expect(userRoutes.get).toHaveBeenCalledWith(
      '/:username',
      usersController.show
    )
  })

  test('post /api/usersにはcreateアクションが呼ばれる', () => {
    expect(userRoutes.post).toHaveBeenCalledWith('/', usersController.create)
  })
})

ここまでで単体テストを書き終えました。

結合テスト

結合テストは、supertestを用いて行います。
supertestは、サーバーを立てずとも擬似的なHTTPリクエストを送ることでテストすることができます。

テストの準備

結合テストを行う前の準備として、src/index.tsのDBに接続している箇所を修正します。
実行環境がtestだった場合には、テスト用のDBに接続するようにしましょう。

src/index.ts
if (process.env.NODE_ENV === 'development') {
  mongoose.connect('mongodb://localhost:27017/express-test', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useCreateIndex: true
  })
} else if (process.env.NODE_ENV === 'test') {
  mongoose.connect((global as any).__MONGO_URI__, {
    useNewUrlParser: true,
    useCreateIndex: true,
    useUnifiedTopology: true
  })
}

さらに、実行環境がtestのときにはサーバーを起動する必要がないので、ここも修正します。

src/index.ts
if (process.env.NODE_ENV !== 'test') {
  app.listen(port, () => {
    console.log('server start')
  })
}

次に、テストで使用するsupertestをインストールします。

npm i -D supertest @types/supertest

テストを記述する

準備ができたら、テストを記述していきましょう。

test/integration/user.spec.tsに記述していきます。

test/integration/user.spec.ts
import request from 'supertest'
import mongoose from 'mongoose'
import app from '../../src/index'
import User from '../../src/Models/user'
import { User as UserType } from '../../src/types/user'

// テストデータ
const users: UserType[] = [
  {
    username: 'user1',
    firstName: 'aaa',
    lastName: 'bbb',
    gender: 'male',
    age: 22
  },
  {
    username: 'user2',
    firstName: 'ccc',
    lastName: 'ddd',
    gender: 'male',
    age: 30
  },
  {
    username: 'user3',
    firstName: 'eee',
    lastName: 'fff',
    gender: 'female',
    age: 34
  }
]

describe('intergration user', () => {
  beforeEach(async () => {
    await User.deleteMany({})
    await User.collection.insertMany(users)
  })

  afterAll(() => {
    mongoose.connection.close()
  })

  describe('GET /api/users/:username', () => {
    test('responds with json', async () => {
      const response = await request(app)
        .get('/api/users/user1') // GETリクエスト
        .set('Accept', 'application/json') // リクエストヘッダー
        .expect('Content-Type', /json/) // レスポンスのContent-Typeが正しいか
        .expect(200) // レスポンスのステータスコードが正しいか

      // レスポンスボディが正しいか
      expect(response.body.user.username).toEqual(users[0].username)
    })
  })

  describe('POST /api/users', () => {
    test('responds with json', async () => {
      const user: UserType = {
        username: 'user4',
        firstName: 'ggg',
        lastName: 'hhh',
        gender: 'female',
        age: 48
      }
      const response = await request(app)
        .post('/api/users') // POSTリクエスト
        .send(user) // POSTデータ
        .set('Accept', 'application/json')
        .expect('Content-Type', /json/)
        .expect(201)

      expect(response.body.user._id).toBeDefined()
    })
  })
})

モデルのテストを記述した際と同様に、テスト毎にテストデータの投入とテスト後のDBの接続の切断を行っています。

テスト対象とするモジュールは/src/index.tsからエクスポートされるモジュールです。
これを、supertest(requestという名前でインポートしています。)の引数として渡すことでメソッドチェーンをする形でリクエストを送ることができます。

テスト内容は以下の点です。

  • 正しいパスにリクエストを送れているか
  • レスポンスのContent-Typeが正しいか
  • レスポンスのステータスコードが正しいか
  • レスポンスのボディが正しいか

おわりに

Expressのアプリケーションのテストについて書いてきました。
モジュールに分解することによりテストが書きやすくなるという利点と、Jestの強力さを感じ取ることができました。

また、テストデータの投入の部分など、テストコードをDRYにするために改良できる点がまだあるかと思います。
ぜひ試してみてください。

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

文字列化された関数を実行する

title

例)
functionOrKey="sort"
value[functionOrKey]()

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

GAS で掲示板をこしらえる

きょうび、自鯖に掲示板を用意したいと思っても、CGI や PHP のプログラムを配布しているサイトは軒並み停止していて、スマホ対応なんて望むべくもなく、もはや自作するしかない……ということで、なぜか Gogle Apps Script でスレッド式掲示板を作ってみました。

そんで、出来上がったものがこちら

ソースコードは GitHub に上げました。
GAS-BBS
GitHub 使うの初めてなので、全ファイルサイト上でコピペしてリポジトリ作りました?

かなり簡素な作りですので、利用したいという奇特な方は、いい感じに改造してからご利用ください。

苦労した点

ぶっちゃけあんまないけど、HTML テンプレート経由だと勝手にエスケープしちゃって br 要素を出力することもできないので、仕方なく本文を CSS で white-space:pre-line して改行が反映されるようにしています。

そんなことより全然使い込んでないので、後々問題が出てくる気はしますが、もはや若人には「え?ビービーエ…なんて?」とか言われちゃう掲示板を作りたい方はチャレンジしてみるのも良いかと思います。

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

引数に最小値(min)最大値(max)を入れるとランダムな整数を返してくれる関数

ランダムな整数が欲しい!

そんなときはこの処理を使おう!
自分がよく使っている関数です。

const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

なんのこっちゃない処理ですが
ブラウザゲーム作成時によく使います。

rand(1, 10) //1〜10までのランダムな整数を返してくれます

いろいろと応用が効くので使いやすい。
DOM操作時でも、例えばランダムに画面外からやってくる感じを演出するときにも使えます。

element.style.transform = `translate(${rand(-100, 100)}%, ${rand(-100, 100)}%)`

とかってしてやると
縦軸、横軸ランダムな位置に配置することができるので、
css animationと組み合わせてやると
ランダムな位置に発生して所定の位置へ移動してくる動きを要素に与えることができます。

const els = document.querySelectorAll('[class^=devicon-]')
      els.forEach(el => {
        el.style.transform = `translate(${rand(-100, 100)}%, ${rand(-100, 100)}%)`
        el.classList.add('icon-move')
        setInterval(() => {
          el.style.zIndex = this.rand(-1, 1)
        }, this.rand(4000, 8000))
      })

4秒から8秒のうちランダムな秒数後にz-indexを上げ下げすることで表示、非表示をコントロールしています。
実際に私のポートフォリオの「study」ページにて使用していますので
ぜひ動きをみてにきてください。

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

Vue.js向けの高機能・高性能なテーブルコンポーネントvxe-tableを紹介したい

何に関する記事か?

:white_check_mark: vxe-table というVue.js向けのテーブルUIコンポーネントを紹介する記事です。
かなり高機能・高性能なライブラリなのですが、中国発ということもあり日本語の情報が見当たらなかったので記事にしてみました。

:warning: この記事ではコンポーネントの提供する機能のほんの一部しか紹介していません。より詳細な情報は以下のリンクから参照してください。

:link: Link

  • GitHub - vxe-table
    • コンポーネントの概要やインストール手順、サンプルコードなど
  • vxe-table 公式ガイド(英語・中国語)
    • コンポーネントの提供する機能・サンプルコードやAPIリファレンスなど
    • かなりたくさんの機能が提供されているので、是非一度チェックしてみてください!

誰にとってオススメか?

Element, Vuetify などのコンポーネントライブラリを使っていて、テーブル(グリッド)コンポーネントで実現に手間のかかる機能がある場合や性能的な問題を抱えている場合にオススメです。
とくにテーブルコンポーネントに多様な機能・性能を求められる業務系のアプリなどで良さを発揮しやすいと思っています。

以下では、vxe-table の基本的な書き方を押さえた後、
Element / Vuetify との簡単な性能比較を行うサンプルを作成しています。

vxe-table の基本的な書き方

install

以下のリンクに沿って、CDNかnpmでインストールしましょう。
https://github.com/x-extends/vxe-table/blob/HEAD/README.en.md#installing

日本語化にも対応されています。
https://x-extends.github.io/vxe-table/#/table/start/i18n

シンプルなテーブルの例

まずは GitHub - Example のシンプルな例から見てみます。
※コメントを追記しています。

<template>
  <div>
    <!-- vxe-tableコンポーネントにtableDataをバインドする -->
    <vxe-table :data="tableData">
      <!-- 各カラムをvxe-table-columnで定義する -->
      <!-- type="seq"で行番号を表示する -->
      <vxe-table-column type="seq" title="Seq" width="60"></vxe-table-column>
      <!-- tableDataのkey名をfieldとして指定する -->
      <vxe-table-column field="name" title="Name"></vxe-table-column>
      <vxe-table-column field="sex" title="Sex"></vxe-table-column>
      <vxe-table-column field="address" title="Address"></vxe-table-column>
    </vxe-table>
  </div>
</template>

<script>
export default {
  data () {
    return {
      // vxe-tableにバインドされるデータ
      tableData: [
        { id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', address: 'Shenzhen' },
        { id: 10002, name: 'Test2', role: 'Test', sex: 'Man', address: 'Guangzhou' },
        { id: 10003, name: 'Test3', role: 'PM', sex: 'Man', address: 'Shanghai' }
      ]
    }
  }
}
</script>

これまでに他のテーブルコンポーネントを利用したことのある方はすぐに理解できると思います。
実装ではこれを基本形として、<vxe-table><vxe-table-column>タグにPropsを追加していく形になります。

JSからのテーブル操作

JSからテーブルを操作したい場合は vxe-table タグに ref を付けて、this.$refs から取得したテーブルコンポーネントからAPIを利用します。
例えば、2行目のチェックボックスをtoggleさせたい場合は以下のようなソースになります。

<template>
  <div>
    <button @click="toggleSecondRow">2行目のチェックボックスを操作</button>

    <!-- テーブルにrefで名前を付ける -->
    <vxe-table ref="myTable" :data="tableData">
      <vxe-table-column type="checkbox" width="60"></vxe-table-column>
      <vxe-table-column field="name" title="Name"></vxe-table-column>
      <vxe-table-column field="sex" title="Sex"></vxe-table-column>
      <vxe-table-column field="address" title="Address"></vxe-table-column>
    </vxe-table>
  </div>
</template>

<script>
export default {
  data () {
    return {
      tableData: [
        { id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', address: 'Shenzhen' },
        { id: 10002, name: 'Test2', role: 'Test', sex: 'Man', address: 'Guangzhou' },
        { id: 10003, name: 'Test3', role: 'PM', sex: 'Man', address: 'Shanghai' }
      ]
    }
  },

  methods: {
    toggleSecondRow() {
      // vxe-tableのAPIを利用してチェックボックスの2行目をtoggleさせる
      this.$refs.myTable.toggleCheckboxRow(this.tableData[1]);
    }
  }
}
</script>

:tada: これで vxe-table のガイドを読むのに最低限必要な準備は完了です。
後は vxe-table 公式ガイド を読めば実現したい機能を実装していけるはずです!

vxe-table と他のコンポーネントライブラリとの性能比較

vxe-table では virtual scroller が標準で組み込まれているため、大量データでも高い性能を発揮します。
個人的にこのライブラリで一番感動したポイントなので、大量データのテーブル実装を行ってみて、簡易的な性能比較をしてみたいと思います。

具体的には、Element / Vuetify / vxe-table でそれぞれ10列×1000行のテーブルデータと行選択できるチェックボックスを実装します。

1. Element

https://jsfiddle.net/Nag729/f3j2txnm/20/

スクロールからすでに遅く、チェックボックスの選択はスムーズとは言い難いですね:cry:
ソートも同様に時間がかかっています。

2. Vuetify

https://jsfiddle.net/Nag729/u5jgqvr4/2/

Element と比較すると優秀で、スクロールにストレスはありません
ただ、全データの選択やカラムソートになると結構待たされます:expressionless:

3. vxe-table

https://jsfiddle.net/Nag729/o13xmpn0/6/

スクロール・チェックボックス選択・ソートの全てがスムーズに動いています:blush:
自前でライブラリや設定を追加しなくてもこれだけの大量データに対応してくれるのは嬉しいですね!

終わりに

以上、vxe-table の紹介でした。
現状でもかなりの機能が用意されている上に、これから先もバージョンアップが予定されているようなので、是非一度使ってみてください。

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

コーダーが独学JavaScriptを駆使してHTMLメルマガの制作業務を自作ツールで駆逐する話

メルマガ業務駆逐ツールを制作中です

作ったツールはこちら:https://automatic-images-slicer.n-haruka.dev/
Githubはこちら:https://github.com/mimosusc/automatic-images-slicer

今のところ可能なのは画像のオートスライスです。
※PCのChrome以外は動作しません(Mac以外未検証)
※いくつかバグがあります

前書き

作っているツール自体開発途中なので記事もまだ未完ですが、技術を学び始めた人が自力で有益なツールを作ったり開発を楽しめるようになるキッカケになればいいなという思いと、文章も書けるようになった方がいいよなあという思いで、書ける範囲で書き切って投下しました。
プログラミングを勉強したものの何を作ればいいのか分からないという人に、何かしらの参考になると嬉しいです。

※この記事は自作ツール開発の軌跡を綴った日記のようなもので、技術的な問題を解決に導くような内容はほぼ含みません。

こういうルーティン作業をしていました

ツールの開発を始めた当時は、主にサイト運用とメルマガ制作を担当していました。

メルマガ制作とは、Photoshopでpng画像を開き、地道にスライスし、スライスした画像間の余白を計り、コードに落とし込む… いわゆるザ☆ルーティンワークです。

※スライスとは、Photoshopで切り出したいところを範囲選択して指定する画像の書き出す方法です(ズレないようにズームしてキワを見極めてやるので、けっこう凝視するし手間がかかります)

面白みが無いうえ目も疲れるし、正直なところ、もっとメルマガ以外のことをやりたいなと思うくらいには苦痛を感じていました。

画像オートスライサーの発案

ある時、いつものようにPhotoshopの画面を見ていて、ふと頭に浮かんだのがHTML5のcanvas要素

canvasをしっかりと使ったことはなかったが、JSでブラウザ上に図を描画できることだけは知っていた。

“描画ができるんなら、ピクセルの色を取得することもできるんじゃないか?”

調べてみると、canvas要素を用いて座標の色を取得する方法がわんさと出てきた。ビンゴビンゴ♪

座標の色を取得できるということは、違う座標の色との比較ができるということ。つまり、理論的には色が切り替わった座標を検知できる

この瞬間、スライス作業自動化に希望の光が差した。

しかし、スライス作業を行えるようにするには矩形範囲選択ができる必要がある。JSでできるだろうか。Chromeで「矩形画像選択 js」と打ち込み、ググった。するとコード付きの詳しい解説記事がヒットした!part3まで連載されていて心強い。これはいける気がする

ヒットしたプチモンテさんの記事:画像をマウスで範囲選択する[Canvasの矩形選択1]
https://www.petitmonte.com/javascript/canvas_select_range.html

※矩形選択範囲に関しては基本的にプチモンテさんで掲載しているコードで実装させていただきました:bow:

コードジェネレーターへの進化

軌道に乗り始めてきたところでアイディアが更に発展する。

メルマガを作る上で案件ごとに異なるのは、主に下記に列挙するものたち。(以降、これらを案件変数と呼びます)

  • 挨拶文テキスト(大抵、デバイスフォントで作る)
  • 画像ファイル、画像のwidth & height
  • 余白のwidth & height
  • 遷移先のリンク

メルマガはほとんどが画像と余白で構築されていて、それらの値を決定づけているのが実はスライス作業。つまり、この作業をブラウザ上で行う事ができれば、必要な案件変数の多くをJS内に保持することができるから、スライス作業のみならず、メルマガコーディング自体を自動化できるかもしれないのだ!(これはデカい!テンションが上がった!)

※この辺りで自動化する事の偉大さに気づきました。これまでは面倒な作業を省きたい一心でしたが、例えば仕事の9割を自動化できたら、極論10%の労働だけで生きていける訳ですよね…(会社員はそうもいきませんが)自分がそれを実現できるかもしれないと思うとすごく自信になりますし、ルーティンでつまらない作業もプログラムにやってもらおうと思うと世界観が別物で、とってもクリエイティブになります(ずっとイージーモードでやらされていたゲームでハードモードを解禁されたような感覚かもしれませんw 技術ももっと身に付くはずです)

仕様を決める

“実現できそう”という判断に至ったので、仕様を決めていく。

当初は画像をアップロードしただけでHTMLを吐くようにしたいと考えていたものの、これはなかなか難しい課題があって非現実的だった。

まず、テキストはデバイスフォントでなければならない箇所が存在するし、案件によって個別に判断が必要なこと。
また、リンクを設定しなければならない要素が存在するが、画像からその要素を認識するのはJSでは難しい。

このように、JSでの解決が容易でない問題が見えてきたので、これらは人間側に判断させる方針にしてアプリのグレードを引き下げた。

※難題とはいえ、何か方法がないかと調べてみました。
着目したのは画像からテキストを読み取ってくれるJSのOCRライブラリ、Tesseract.jsです。
https://co.bsnws.net/article/198
日本語にも対応しています。英語の精度はかなりのものですが、日本語はもう一押しというところ。安全策をとって導入しませんでした。
また、機械学習の処理を可能にするJSライブラリがあるようです。
https://avinton.com/blog/2019/07/tensorflow-js/
画像分類もできるようなので、ボタンを認識させることも可能なのかもしれません。ライブラリの理解に時間がかかるかなと思い、今回は導入しませんでした(まずは完成を最優先)
どちらも、いずれご縁があったら使ってみたいと思います^^

よし、最終的に下記の仕様で確定した(ざっくり)

  • 選択範囲機能でスライスしたい要素を囲むと、対象の要素にスナップする(吸着する)
  • スナップ済み要素を確認できる
  • スナップ済み要素上で右クリックすると独自のコンテキストメニューを表示。そこからデバイスフォント指定や、リンク指定など、人間側で判断する事項を指定できる
  • スナップ済み要素を画像にして一括ダウンロードできる
  • メルマガ用コードをビルドして出力する

あとは、コードをもりもり書いて形にするだけ。

“画像の劣化”という壁

矩形選択範囲はプチモンテさんのブログ記事を参考にしながらなんとか実装が終わった。
※私が序盤に実装したい機能が詰まっていたから、本当に有り難かったです。

次のステップは、“対象の要素にスナップする”。
ここからは自分で考えて書いていく必要がある。とはいえ、“こうすればイケるだろう”という案があったので、着手自体は辛くない。アテが外れてからが戦い。はい。案の定アテが外れました 笑
※ツールの詳しい実装方法は別記事で掲載予定です

書き出された後の画像は、わずかながら劣化している。単色の中にスライスしたい要素がある場合、まずは要素を囲った選択範囲の上辺から下に向かって色の切り替わりを探知していく感じになるが、人間の目では同じ単色に見えていても実際はそうではない。それを見越して、色そのものの比較ではなくどの位の割合で一致していたかというパーセンテージによる検知で実装したのだが、それでも感知がシビアすぎた。ひと工夫して、もっと人間の視覚に寄せていく必要がある。

そこで、作戦を練り直した。

ピクセルの横一行の色を先に全て取得し、最も割合が多かった色を”メインカラー”とする。
そして、“許容値”という値を変数で新たに設け、適度な数値を入れておく(15にした)。次に、メインカラーとピクセルの横一行の色を順繰りに比較していく。色の差が許容値の範囲内なら、そのピクセルの色をメインカラーと同じ色とみなす作戦だ。“許容値”という概念を盛り込むことで認識精度のコントロールが出来るようになり、人間の認識に近い精度を実現することができた。
※ちなみに、最初はメインカラーを決めず、隣接するピクセルの色を許容値で比較する実装を考えていました。しかし、この実装方法だと基準となる色が次々と変動してしまいます。それだとバグの原因になりそうなので、基準となる色をあらかじめ決める方針に軌道修正しました。

この色識別処理をスナップしたい要素の前後左右にかけることで、オートスライスが可能となった。
次のステップは“コンテキストメニューを設置して人間が入力した指定データを保持する”の実装だ…

※1 色の差はどういう基準で判別している?:RGBの合計値の差の絶対値です。この絶対値が許容値以下であれば、同じ色と認識する仕組みになっています。

※2 許容値が15である理由:Photoshop上で要素の色が着き始めたピクセルと要素でないピクセルの色の差※1を比較すると、小さなもので30ほどの違い。対して、劣化による色の差は大きくて10程度。間を取って15としました。

Comming Soon!

実装中です:コンテキストメニューを設置して人間が入力した指定データを保持する

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

JAVASCRIPT 開発者を採用する方法

JavaScript言語は、JavaおよびC言語に基づいた構成を持っている、強力なクライアント側のマルチパラダイムの動的言語です。それに多数のタイプとオペレーター、組み込みオブジェクトやメソッドが含まれています。JavaScriptはオブジェクト指向プログラミングと関数型プログラミングの両方をサポートしていますので、1つの言語内でほぼどんなオブジェクトまたは機能でも作成できます。

JavaScriptの起源と最新のトレンドに詳しいJavaScript プログラマーを採用する方法

JavaScriptは単独で実行不可能であり、JavaScriptコードを実行するブラウザーが存在します。 ユーザーがJavaScriptの有効なHTMLページを開こうとすると、スクリプトがブラウザーに送信され、ブラウザーはスクリプトの下で動作します。ブラウザー以外に、JavaScriptはAdobeサービス、サーバーサイド環境、データベース、SVG画像などで表示することもできます。JavaScript言語は幅広いタイプのアプリケーションに使用できます。
javascript-kaihatsusha-infographic.png

JavaScript開発の基本、利点やトラップ

2018年にリリースされたState of the Developer Ecosystemのレポートによると、JavaScriptは3年連続で、世界で最も使用されているプログラミング言語であると認識されました。この調査は、世界中の17か国の6千人のプログラマーを対象に実施されました。

JavaScriptプログラミング言語を使用し、他の言語の専門家の代わりにJavaScript プログラマーを採用する利点と欠点を説明します。間違いなく、利点の方が多いです。
javascript-kaihatsusha-no-tansho-to-chousho.png

Javascript プログラマー コスト

PayScaleによるトップ5か国のフリーランスベースでのJavaScript エンジニアの平均年間報酬をご覧ください。
javascript-developer-cost.png
その国の一般的なJavaScript プログラマーの時給を詳しく見てみると、1時間あたりの報酬はイギリスが一番高いことが明らかになります。

それでも、ウクライナのフリーランスJavaScript 開発者の時給は、JavaScriptプログラマーが勤務ないしは居住する上位国の中で最低です。ウクライナからのJavaScriptプログラマーが技術的知識と創造力において、世界中で非常に人気であるということはよく知られています。したがって、トップクラスのフリーランスJavaScriptエンジニアを採用する予定がある場合は、ウクライナのコーダーの採用をご検討ください。

ウクライナの開発者にご興味のある方は、以下のリンクをご参照ください。
https://jp.mobilunity.com/blog/hire-javascript-developer-jp/

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

Resolve error: No valid exports main found for /node_modules/colorette の解決法

Vue.js書いていてコミットしようとしたら、 vue-cli-service lint で怒られた。

✖ vue-cli-service lint found some errors. Please fix them and try committing again.
Error resolving webpackConfig Error: No valid exports main found for '/path/to/project/node_modules/colorette'

なんのこっちゃ・・・と思って調べたら、Node.jsのバージョンが古いのが問題らしい。
v13.5.0 でした。

別PCで作ったプロジェクトをクローンしたときに、差異が発生したのだろうか。

nvmが入っていたので、この記事を参考にバージョン上げ。

nvm ls-remote

で最新バージョンを確認。

nvm install v14.7.0

で最新バージョンをインストール。

プロジェクト内の node_modules を削除し、改めてパッケージをインストール。

npm install

無事、コミットできるようになりました:v:

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

vte.cxによるバックエンドを不要にする開発(8.gitフローと自動デプロイ)

前回=> vte.cxによるバックエンドを不要にする開発(7.サーバサイドJavaScript)

今回はvte.cxにおけるGitフローとGithub Actionsによる自動デプロイの設定方法について説明します。

vte.cxにおけるGitフロー

vte.cxはBaaSなので開発環境を容易に作成できます。
管理画面のサービスの作成ボタン一つで、データベースを含む全く同じ環境を作成できるのです。開発環境構築にかかる工数は実質的にゼロなので、複数人が開発する場合は、各自がそれぞれの開発環境を自由に作成していくことができます。これまでの開発では、開発環境の構築に工数と時間がかかり、いざ利用したいときにも他の開発者との多くの調整が必要だったのですが、vte.cxではそれが全く不要になります。

以下は、vte.cxにおけるGitフローを説明した図です。右側から説明しましょう。

単体開発・テストでは、各自がdevelopからwork-{ニックネーム}というブランチを作成し、そこで単体機能の開発とテストを行います。vte.cxの環境は、ローカルで確認できる環境以外にリモートで実際に動かして確認できる環境(例えば、{ニックネーム}.vte.cx)を作成できます。この環境は基本的に作成した人以外は使わないので、クローズドな環境で自分のペースで開発ができます。つまり、各自に開発環境が与えられるので開発環境がボトルネックになることはなく並行開発ができるようになるのです。

スクリーンショット 2020-08-10 17.16.23.png

次に、単体テストが終わったものはfeature-xxというブランチにマージします。xxはGitHub Issueの番号を入れるようにしています。このようにしているのは検証環境で機能ごとに統合テストを行えるようにするためです。

実際にfeature-xxをreleaseブランチにマージすることで検証機に対して自動的にデプロイが実行されます。feature-xxで改修した内容を検証環境で確認して問題なければmasterブランチにマージします。このとき、本番環境に対して自動デプロイが実行されます。

この方法のよいところは、機能ごとに独立してデプロイを行える点です。
例えば、ある機能(feature-xx)を最初に着手していたけれども、もう一つの機能(feature-yy)の方が先に進んでテストも終わったという場合、feature-xxをスキップしてfeature-yyを本番機にデプロイすることが可能です。つまり、どこかの機能がボトルネックになって次の改修のリリースができないといったことがなくなるわけです。
また、機能を細かく分解することでリリースの頻度を上げることができるようになります。

緊急の修正などについては、hotfix-xxというブランチをmasterから作成することで別途対応します。

自動デプロイの設定

自動デプロイはGithub Actionsを利用しています。

.github/workflows/deploy.ymlを見ていただければわかるように、npm installしてwebpackでビルド&デプロイを実行します。

# This is a basic workflow to help you get started with Actions

name: master

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
  push:
    branches: [ master ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [10.x]
    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
    # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
    - uses: actions/checkout@v1
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm install
      env:
        CI: true
    - run: cp .confy ~/.
    - run: git log -m -1 --name-only --pretty="format:" | ./deploy_auto.sh

cp .confy ~/.では、アクセストークンの設定ファイルのコピーを行っています。(※ アクセストークンは機密情報なのでgithubに登録する際は必ずpriveteにしてください。publicには登録しないでください。)
コマンドラインで、npm run loginを実行してサービスにログインすると、~/.confyにアクセストークンの情報が書き込まれます。それをgithubのホームディレクトリに置いておく(.confy)ことで、上記コピーコマンドでCIのビルド時に参照できるようにしています。

最後の、git log -m -1 --name-only --pretty="format:"は、マージ元のファイルの一覧を表示するもので、それらファイル名を元に、./deploy_auto.shで実際にvte.cxにデプロイする仕組みになっています。

.github/workflows/deploy.yml.confy./deploy_auto.shをgithubに置くだけで自動デプロイ環境ができるので、皆さんもぜひ試してみてください。

それでは。

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

browser-syncでAPIをproxyする

始めに

ローカルでの開発でAPIのCORSを回避するためにproxyを通すことがあると思いますが、webpack-dev-serverを使うと簡単に設定することができます。また、検索にも引っ掛かりやすいのでつまづいたら調べることができます。

webpack.config.js
devServer: {
  proxy: {
    '/api': {
      target: 'https://hogehoge.com',
      changeOrigin: true,
    },
  },
}

しかしbrowser-syncを使ったパターンになると中々記事がない上、browser-syncで使用するパラメータのproxyはwebpack-dev-serverとは違った意味で使われるため中々使い方が分かりませんでした。なのでその辺についてここでまとめたいと思います。

browser-syncで設定するproxyの勘違い

browser-syncで設定するproxyはwebpack-dev-serverと違ってローカルサーバーのhostnameを指定するものになっています。localhostではなくhogehoge.comでアクセスできるようにするとかですね。あんまりどういう目的で使用するのか分かりませんが・・・。
なのでAPIを別なサーバーへproxyしたいときとかには使用できないです?

browser-syncでAPIをproxyする方法

proxyオプションで設定できないのでどうするかと言うと、proxy用のmiddlewareを差し込みます。
具体的にはhttp-proxy-middlewareを使用して以下のようにしました。
ちなみにwebpack-dev-serverもこのパッケージを使用しているので設定方法は同じになると思います。
https://webpack.js.org/configuration/dev-server/#devserverproxy

script.js
const browserSync = require('browser-sync');
const { createProxyMiddleware } = require('http-proxy-middleware');

const bs = browserSync.create();

bs.init({
  server: {
    baseDir: './src',
    middleware: [
      createProxyMiddleware('/api', {
        target: 'https://qiita.com/',
        // Basic認証がある場合はユーザ名とパスワードをコロンでつなげると自動で認証してくれる
        // auth: 'username:password',
        changeOrigin: true,
      }),
    ],
  },
  watch: true,
  ghostMode: false,
});

あとは普通にbrowser-syncで立ち上げたサーバーにAPIリクエストしたらproxyしてくれます。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Proxy Test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
</head>
<body>
  <p>proxy test</p>
  <script>
    axios.get('/api/v2/items')
      .then((response) => {
        console.log(response.data);
      });
  </script>
</body>
</html>

終わりに

以上がbrowser-syncでAPIをproxyする方法でした。普段の開発ではwebpack-dev-serverでproxyするでいいと思いますが、webpackすらいらない場合の開発やビルド済みのファイルでローカルで動作確認する際にbrowser-syncでproxyする方法は役立つかなと思いました。
以下にサンプルのリポジトリも載せますので、興味がある方は見てください。

参考URL

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

【Nuxt】SSGとSPAの『nuxt generate』やアプリケーションの挙動の違いについて調べてみる

以前、【Nuxt】SSR・SSG・SPAにおける『nuxt build』と『nuxt generate』の実行結果の違いまとめで、Nuxtで用意されているSSR(Server Side Rendering)、SSG(Static Site Generator)、SPA(Single Page Application)の計3つのモードについて紹介しました。

静的ウェブサイトで運用する場合はSSGかSPAを選択できます。

SSGとは事前に生成した静的ファイルを利用して画面を表示するモードです。

SSGは事前に各画面の静的ファイルを生成するため画面表示が早い反面、コンテンツを更新するたびにビルドが必要という欠点があります。

ランディングページをはじめとした更新頻度の少ないページとSSGは相性がよいです。
一方、同じURLでもユーザーによって画面の見え方の異なるページや、更新頻度の多いページとの相性はあまりよくありません。

SPAとはサーバーサイドでレンダリングを行わず、フロントエンドで画面を組み立てるモードです。

SPAは差分更新のみで画面を変更するため画面遷移が高速である反面、JavaScriptを読み込むため初回のページロードに時間がかかるという欠点があります。

SSGとSPAの実装は異なりますが、『nuxt generateで生成された成果物を静的ウェブサイトへデプロイする』というプロセスは一緒です。
SSGとSPAの定義がわかっていてもプロセスが同じであるため、言葉だけだと違いがイマイチわかりにくいです。

そこで今回はnuxt generateによる成果物やNuxtアプリケーションの挙動がどのように異なるのか具体例をもとにSSGとSPAの違いについて紹介します。

なお、Nuxtは2.14.1を利用します。

検証に利用するサンプルアプリケーションについて

SSG・SPAにおけるnuxt generateおよびNuxtアプリケーションの挙動を確認するにあたり、今回はNuxt公式ドキュメントで紹介されているCustom Routesを利用します。

Custom Routesはユーザー一覧画面と各ユーザーの詳細画面が用意されているシンプルなアプリケーションです。

ユーザー一覧画面↓

スクリーンショット 2020-08-10 15.57.45.png

ユーザー詳細画面↓

スクリーンショット 2020-08-09 17.11.56.png

『nuxt generate』による成果物の違いについて

nuxt generateの実行プロセスや成果物の違いについて紹介します。

SSGの場合

nuxt generateの実行ログは以下の通りです。
Generated route "/users/xx"というログからわかるように、各ページの静的ファイルが生成されています。

ℹ Doing webpack rebuild because nuxt.config.js modified
ℹ Production build
ℹ Bundling for server and client side
ℹ Target: full static
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 6.12s

✔ Server
  Compiled successfully in 624.21ms


Hash: 05e1720be351433f6124
Version: webpack 4.44.1
Time: 6121ms
Built at: 2020/08/10 14:20:38
                          Asset       Size  Chunks                         Chunk Names
 ../server/client.manifest.json   7.86 KiB          [emitted]
                       LICENSES  389 bytes          [emitted]
                 app.b67a212.js   58.5 KiB       0  [emitted] [immutable]  app
node_modules/commons.501805f.js    168 KiB       1  [emitted] [immutable]  node_modules/commons
         pages/index.fe29326.js   1.51 KiB       2  [emitted] [immutable]  pages/index
     pages/users/_id.7d0c948.js   1.45 KiB       3  [emitted] [immutable]  pages/users/_id
             runtime.715f042.js   2.35 KiB       4  [emitted] [immutable]  runtime
 + 2 hidden assets
Entrypoint app = runtime.715f042.js node_modules/commons.501805f.js app.b67a212.js

Hash: 93b14cb09a45b6c86e9a
Version: webpack 4.44.1
Time: 625ms
Built at: 2020/08/10 14:20:39
               Asset       Size  Chunks             Chunk Names
      pages/index.js   6.63 KiB       1  [emitted]  pages/index
  pages/users/_id.js   6.59 KiB       2  [emitted]  pages/users/_id
           server.js   87.2 KiB       0  [emitted]  app
server.manifest.json  307 bytes          [emitted]
 + 3 hidden assets
Entrypoint app = server.js server.js.map
ℹ Generating output directory: dist/
ℹ Generating pages with full static mode
✔ Generated route "/"
✔ Generated route "/users/1"
✔ Generated route "/users/2"
✔ Generated route "/users/10"
✔ Generated route "/users/4"
✔ Generated route "/users/6"
✔ Generated route "/users/5"
✔ Generated route "/users/7"
✔ Generated route "/users/9"
✔ Generated route "/users/8"
✔ Generated route "/users/3"
✔ Client-side fallback created: 200.html
✨  Done in 10.77s.

静的ファイルの出力先であるdist配下は以下のようになっています。
ルートのindex.htmlだけでなく、ユーザー詳細画面(users/:id/index.html)の静的ファイルも存在しています。

$ cd dist
$ tree -L 6

.
├── 200.html
├── README.md
├── _nuxt
│   ├── LICENSES
│   ├── app.b67a212.js
│   ├── node_modules
│   │   └── commons.501805f.js
│   ├── pages
│   │   ├── index.fe29326.js
│   │   └── users
│   │       └── _id.7d0c948.js
│   ├── runtime.715f042.js
│   └── static
│       └── 1597036839
│           ├── payload.js
│           └── users
│               ├── 1
│               │   └── payload.js
│               ├── 10
│               │   └── payload.js
│               ├── 2
│               │   └── payload.js
│               ├── 3
│               │   └── payload.js
│               ├── 4
│               │   └── payload.js
│               ├── 5
│               │   └── payload.js
│               ├── 6
│               │   └── payload.js
│               ├── 7
│               │   └── payload.js
│               ├── 8
│               │   └── payload.js
│               └── 9
│                   └── payload.js
├── favicon.ico
├── index.html
└── users
    ├── 1
    │   └── index.html
    ├── 10
    │   └── index.html
    ├── 2
    │   └── index.html
    ├── 3
    │   └── index.html
    ├── 4
    │   └── index.html
    ├── 5
    │   └── index.html
    ├── 6
    │   └── index.html
    ├── 7
    │   └── index.html
    ├── 8
    │   └── index.html
    └── 9
        └── index.html

SPAの場合

nuxt generateの実行ログは以下の通りです。
SPAの場合はSSGと違い、静的ファイルはルート(/)のみ作成されています。

ℹ Doing webpack rebuild because nuxt.config.js modified
ℹ Production build
ℹ Bundling only for client side
ℹ Target: static
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 6.96s


Hash: 47d36e3c6f1bf3363c94
Version: webpack 4.44.1
Time: 6961ms
Built at: 2020/08/10 14:18:13
                          Asset       Size  Chunks                         Chunk Names
 ../server/client.manifest.json   7.79 KiB          [emitted]
                       LICENSES  389 bytes          [emitted]
                 app.ef33196.js   55.4 KiB       0  [emitted] [immutable]  app
node_modules/commons.48315ec.js    168 KiB       1  [emitted] [immutable]  node_modules/commons
         pages/index.9ce75e2.js   1.51 KiB       2  [emitted] [immutable]  pages/index
     pages/users/_id.5722ee2.js   1.45 KiB       3  [emitted] [immutable]  pages/users/_id
             runtime.84feac3.js   2.35 KiB       4  [emitted] [immutable]  runtime
 + 1 hidden asset
Entrypoint app = runtime.84feac3.js node_modules/commons.48315ec.js app.ef33196.js
ℹ Generating output directory: dist/
ℹ Generating pages
✔ Generated route "/"
✔ Client-side fallback created: 200.html
✨  Done in 11.78s.

静的ファイルの出力先であるdist配下は以下のようになっています。
SPAではユーザー詳細画面(users/:id/index.html)の静的ファイルは存在していません。

$ cd dist
$ tree -L 4

.
├── 200.html
├── README.md
├── _nuxt
│   ├── LICENSES
│   ├── app.ef33196.js
│   ├── node_modules
│   │   └── commons.48315ec.js
│   ├── pages
│   │   ├── index.9ce75e2.js
│   │   └── users
│   │       └── _id.5722ee2.js
│   └── runtime.84feac3.js
├── favicon.ico
└── index.html

Nuxtアプリケーションの挙動の違いについて

実際に起動させたアプリケーションを例に、挙動の違いについて紹介します。

SSGの場合

実際の画面がプレビューで表示されていることから分かるとおり、SSGではレンダリングされた画面がレスポンスとして返ってきます。

image.png

レスポンスデータにも具体的な値がすでに書き込まれています。

image.png

ユーザー詳細画面も同様です。
image.png

このように、SSGでは各画面の静的ファイルを事前に生成しておきレスポンスとして返します。
ユーザー情報なども静的な情報として書き込まれているため、SSGではデータの更新がある度に静的ファイルを再生成する必要があります。

SPAの場合

実際の画面がプレビューに表示されていないことから分かるとおり、SPAでは画面の作成はフロントエンド(ブラウザ)で行っています。

image.png

SSGではレスポンスに具体的な値が書き込まれていましたが、SPAの場合はJavaScriptが組み込まれています。

image.png

ユーザー詳細画面も同様です。

image.png

このように、SPAではJavaScriptの埋め込まれたindex.htmlをレスポンスとして返します。
フロントエンドでJavaScriptを読み込むことで画面を表示したり画面遷移を実現したりしています。

まとめ

以上で具体例を用いたSSG・SPAの違いの紹介を終わります。

  • 今回のまとめ
    • SSGでは『nuxt generate』で各画面の静的ファイルが生成される
    • SAPでは『nuxt generate』で生成される静的ファイルはルートのみ
    • SSGでは静的ウェブサイトからのレスポンス時に画面が作成されている
    • SPAでは画面の作成はブラウザ上で行われる

参考記事

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

アップロードされたファイルの拡張子とサイズをチェックする方法

  • 環境
    • CentOS Linux release 7.8.2003 (Core)
    • Eclipse IDE for Enterprise Java Developers.Version: 2020-03 (4.15.0)
    • openjdk version "11.0.7" 2020-04-14 LTS
    • JSF 2.3.9

やりたいこと

  1. アップロードされたファイルの拡張子が指定のもの以外の場合はエラーにしたい
  2. アップロードされたファイルのサイズが指定より大きかった場合はエラーにしたい
  3. エラーメッセージは親画面で指定したい

Fileインターフェースから名前やサイズを取り出してチェック

File オブジェクトは特別な種類の Blob オブジェクトであり、 Blob が利用できる場面ではどこでも利用できます。
File - Web API | MDN

ファイルの拡張子が指定のもの以外の場合はエラーにしたい

upload.js
/**
 * 拡張子が正しいか判定する.
 * @param  {string} ファイル名.
 * @return {Boolean} true:正しい.
 */
function isCorrectExtension(name) {
    // スペース以外の文字で始まって「.jpg」「.png」「.gif」「.psf」で終わる文字(大文字・小文字を区別しない[i])
    var format = new RegExp('([^\s]+(\\.(jpg|png|gif|pdf))$)', 'i');
    return format.test(name);
}
特殊文字 意味
^ 入力の先頭にマッチ
$ 入力の末尾にマッチ
\s スペース、タブ、改ページ、改行を含むホワイトスペース文字にマッチ

ファイルのサイズが指定より大きかった場合はエラーにしたい

upload.js
/**
 * ファイルサイズが正しいかを判定する.
 * @param  {number} ファイルサイズ(バイト単位).
 * @return {Boolean} true:正しい.
 */
function isCorrectSize(size) {
    /** @type {number} 許容する最大サイズ(1MB). */
    var maxSize = 1024 * 1024;
    return size <= maxSize;
}

エラーメッセージは親画面で指定したい

状況に合わせて使えるように思い付いた方法3つ

方法1. JavaScriptのwindow.openerで親画面から取得する

  1. エラーメッセージを親画面の隠し項目で設定しておく
  2. 子画面のJavaScript処理でwindow.openerを使って取得する
親画面
...省略...
<h:inputHidden id="extErrMessage" value="拡張子が対象外だよ。" />
<h:inputHidden id="sizeErrMessage" value="ファイルサイズが大きすぎるよ。" />
...省略...
upload.js
...省略...
        if (!isCorrectExtension(file.name)) {
            errMessage += window.opener.$('#extErrMessage').text();
        }
        if (!isCorrectSize(file.size)) {
            if (errMessage != '') {
                errMessage += '<br />';
            }
            errMessage += window.opener.$('#sizeErrMessage').text();
        }
...省略...

方法2. 子画面を表示するときにパラメータでメッセージを渡す

  1. 親画面で子画面を表示するJavaSctiptを生成するときにエラーメッセージをGETのパラメータで設定する
  2. 子画面を開いたらパラメータをf:viewParamで受け取ってバッキングビーンに設定する
  3. バッキングビーンのエラーメッセージをJSON形式で置いておく
  4. JavaScriptでparseJSONを使ってエラーメッセージを取得する
親画面
...省略...
<input type="button" value="アップロード" onclick="#{uploadBean.onClick}" />
...省略...
UploadBean.java
    /**
     * onClick属性用に出力するJavaScriptコードを取得する.
     * @return JavaScriptコード.
     */
    public String getOnClick() {
        StringBuilder builder = new StringBuilder();
        builder.append("window.open('upload.jsf");
        builder.append("?key=");
        builder.append(urlEncode("formId:file"));
        builder.append("&extErrMessage=");
        builder.append(urlEncode("拡張子が対象外だよ。"));
        builder.append("&sizeErrMessage=");
        builder.append(urlEncode("ファイルサイズが大きすぎるよ。"));
        builder.append("', '', 'width=500,height=100'); return false;");
        return builder.toString();
    }

    /**
     * orgをURLエンコードして返す.
     * @param org
     * @return
     */
    private String urlEncode(String org) {
        try {
            return URLEncoder.encode(org, "utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
upload.xml(子画面)
...省略...
  <f:metadata>
    <ui:remove>GETのパラメータを受け取る</ui:remove>
    <f:viewParam name="key" value="#{uploadBean.key}"/>
    <f:viewParam name="extErrMessage" value="#{uploadBean.extErrMessage}" />
    <f:viewParam name="sizeErrMessage" value="#{uploadBean.sizeErrMessage}" />
  </f:metadata>
  <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
  <h:outputScript library="js" name="upload.js"/>
  <ui:remove>エラーメッセージをJSON形式で置いておく</ui:remove>
  <script id="errMessage" type="application/json">
    {"ext" : "#{uploadBean.extErrMessage}", "size" : "#{uploadBean.sizeErrMessage}"}
  </script>
...省略...
upload.js
...省略...
        /** @type {array} headタグ内に置いておいたエラーメッセージ. */
        var message = $.parseJSON($('#errMessage').html());
        /** @type {object} 選択されたファイル. */
        var file = inputFile.files[0];
        if (!isCorrectExtension(file.name)) {
            errMessage += message.ext;
        }
        if (!isCorrectSize(file.size)) {
            if (errMessage != '') {
                errMessage += '<br />';
            }
            errMessage += message.size;
        }
...省略...

方法3. 親子画面で同じバッキングビーンを使う

  1. 親子画面で共通のバッキングビーンにエラーメッセージ取得処理を実装する
  2. あとは「方法2. 子画面を表示するときにパラメータでメッセージを渡す」の「バッキングビーンのエラーメッセージをJSON形式で置いておく」以降と同じ
UploadBean.java
...省略...
    /**
     * 拡張しでエラーになった時のエラーメッセージを取得する.
     * @return エラーメッセージ.
     */
    public String getExtErrMessage() {
        return "拡張子が対象外だよ。";
    }

    /**
     * サイズでエラーになった時のエラーメッセージを取得する.
     * @return エラーメッセージ.
     */
    public String getSizeErrMessage() {
        return "ファイルサイズが大きすぎるよ。";
    }
...省略...
upload.xml(子画面)
  <ui:remove>エラーメッセージをJSON形式で置いておく</ui:remove>
  <script id="errMessage" type="application/json">
    {"ext" : "#{uploadBean.extErrMessage}", "size" : "#{uploadBean.sizeErrMessage}"}
  </script>

実装全体

親画面
<?xml version='1.0' encoding='UTF-8' ?>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
  xmlns:h="http://xmlns.jcp.org/jsf/html"
  xmlns:f="http://xmlns.jcp.org/jsf/core">
<ui:composition template="template.xhtml">
<ui:define name="js">
  <h:outputScript library="js" name="upload.js"/>
</ui:define>
<ui:define name="content">
  <h3>ファイルの入力チェックをしてみる</h3>
  <h:form id="formId">
    <div id="uploadArea">
      <ui:fragment rendered="#{!uploadBean.upload}">
        <h:button value="アップロード" onclick="showPopup();"/>
        <h:inputText id="file" style="display:none;">
          <f:ajax event="change" execute="@form" render="@form" listener="#{uploadBean.uploadFile}" />
        </h:inputText>
      </ui:fragment>
      <ui:fragment rendered="#{uploadBean.upload}">
        <h:outputText value="#{uploadBean.file.name}" />
        <h:commandButton value="削除">
          <f:ajax execute="@form" render="@form" listener="#{uploadBean.deleteFile}" />
        </h:commandButton>
      </ui:fragment>
      <div><h:message for="uploadArea" errorClass="error" warnClass="warn" infoClass="info" /></div>
    </div>
  </h:form>
</ui:define>
</ui:composition>
</html>
upload.xhtml(子画面)
<?xml version='1.0' encoding='UTF-8' ?>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
  xmlns:h="http://xmlns.jcp.org/jsf/html"
  xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
  <title>アップロードするファイル</title>
  <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
  <h:outputScript library="js" name="upload.js"/>
  <ui:remove>エラーメッセージをJSON形式で置いておく</ui:remove>
  <script id="errMessage" type="application/json">
    {"ext" : "#{uploadBean.extErrMessage}", "size" : "#{uploadBean.sizeErrMessage}"}
  </script>
</h:head>
<body>
  <div>
    <h:inputFile id="inputFile" onchange="checkFile(this)" value="uploadBean.file" />
  </div>
  <div>
    <h:button value="OK" onclick="submit('#{uploadBean.key}');" />
    <h:button value="閉じる" onclick="window.close();" />
  </div>
</body>
</html>
upload.js
/** ポップアップ画面を表示する. */
function showPopup() {
    window.open('upload.jsf', '', 'width=500,height=100');
}
/**
 * アップロードされたファイルをチェックする.
 * @param {Object} Fileオブジェクト.
 */
function checkFile(inputFile) {
    // エラーメッセージを削除する.
    $('.errMessage').remove();
    /** @type {String} 表示するエラーメッセージ. */
    var errMessage = '';
    if (inputFile.files && inputFile.files[0]) {
        /** @type {array} headタグ内に置いておいたエラーメッセージ. */
        var message = $.parseJSON($('#errMessage').html());
        /** @type {object} 選択されたファイル. */
        var file = inputFile.files[0];
        if (!isCorrectExtension(file.name)) {
            errMessage += message.ext;
        }
        if (!isCorrectSize(file.size)) {
            if (errMessage != '') {
                errMessage += '<br />';
            }
            errMessage += message.size;
        }
    }
    if (errMessage != '') {
        // エラーメッセージを追加する.
        $('#inputFile').after('<br /><span class="errMessage" style="color: red;">' + errMessage + '</span>');
        // ファイルを削除する.
        inputFile.value = null;
    }
}

/**
 * 拡張子が正しいか判定する.
 * @param  {string} ファイル名.
 * @return {Boolean} true:正しい.
 */
function isCorrectExtension(name) {
    var format = new RegExp('([^\s]+(\\.(jpg|png|gif|pdf))$)', 'i');
    return format.test(name);
}

/**
 * ファイルサイズが正しいかを判定する.
 * @param  {number} ファイルサイズ(バイト単位).
 * @return {Boolean} true:正しい.
 */
function isCorrectSize(size) {
    /** @type {number} 許容する最大サイズ(1MB). */
    var maxSize = 1024 * 1024;
    return size <= maxSize;
}

/**
 * 親画面の要素を更新して画面を閉じる.
 * @param  {string} key 更新する親画面要素のid.
 */
function submit(key) {
    window.opener.$('#'+key.replace(/:/g,"\\:")).change();
    window.close();
}
UploadBean.java
package brans;

import java.io.IOException;
import java.io.Serializable;

import javax.faces.view.ViewScoped;
import javax.inject.Named;
import javax.servlet.http.Part;

import lombok.Data;

@Named
@ViewScoped
@Data
public class UploadBean implements Serializable {
    /** serialVersionUID. */
    private static final long serialVersionUID = -355651229394801584L;
    /** ファイルデータ. */
    private Part file;

    /**
     * ファイルがアップロードされているかを判定する.
     * @return true:アップロードされている.
     */
    public boolean isUpload() {
        return this.file != null;
    }

    /**
     * 拡張しでエラーになった時のエラーメッセージを取得する.
     * @return エラーメッセージ.
     */
    public String getExtErrMessage() {
        return "拡張子が対象外だよ。";
    }

    /**
     * サイズでエラーになった時のエラーメッセージを取得する.
     * @return エラーメッセージ.
     */
    public String getSizeErrMessage() {
        return "ファイルサイズが大きすぎるよ。";
    }

    public String getKey() {
        return "formId:file";
    }

    public void uploadFile() throws IOException {
        if (!isUpload()) {
            return;
        }
        if (!isCorrectExtension(this.file.getName())) {
            deleteFile();
        }
        if (!isCorrectSize(this.file.getSize())) {
            deleteFile();
        }
    }

    /**
     * アップロードしたファイルを削除する.
     * @throws IOException エラーが起きた.
     */
    public void deleteFile() throws IOException {
        this.file.delete();
    }

    /**
     * 拡張子が正しいか判定する.
     * @param name ファイル名.
     * @return true:正しい.
     */
    private boolean isCorrectExtension(String name) {
        if (name != null) {
            return name.matches("([^\\s]+(\\.(?i)(jpg|png|gif|pdf))$)");
        }
        return true;
    }

    /**
     * ファイルサイズが正しいかを判定する.
     * @param size ファイルサイズ(バイト).
     * @return true:正しい.
     */
    private boolean isCorrectSize(long size) {
        long maxSize = 1024 * 1024;
        return size <= maxSize;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebRTCで快適な画面共有を 基本編

友人にゲーム画面を共有することが多いのですが、SkypeでもDiscordでもZoomでもSteamのブロードキャストでも、低解像度だったり、解像度は取れていてもビットレートの関係で文字すら読めなかったり(画質が悪い)、遅延がそこそこ長かったりします。
配信知識ゼロから、この一週間で勉強したので、諸々まとめです。
この記事はやり方を説明するものではないのであしからず。

高画質、低遅延配信がしたい!

まあ普通にYoutubeに限定配信してURLを友人に教えれば見る側で手軽に画質変えられるし便利なんですけれど.... なんか自分でやりたくなってしまいました。
ポートの開放ができるなら、RTMPをグローバルIPで外からアクセスさせたり、HLS,MPEG-DASHでCMAF chunkを活用したりと選択肢はあります。
しかし自分の環境下ではポートの開放ができないので、動的に空いてるところを使ってもらうためにブラウザ上でWebRTCを試してみました。
WebRTCのページ共有にngrokを使いましたが、友人側にhtmlを渡してローカルサーバを立ててもらえば不要です。ローカルかhttpsであればいいので、当然Github Pagesとかでもいいです。
ひとまずXSplit Broadcasterを入力として使います。ロゴ入りますが無料でも十分です。
OBS Virtual cameraでもいいですが、XsplitはCPU使用率OBS比で3割くらいで済みます。
今回はSkyway SDKを使わせていただきました。(TURN月500GBまでなら無償で使える)
自分は一対一の共有を想定していたため、TURNサーバの使用のみでSFUはオフにしました。
※テストしたら、自分と友人の場合はTURNサーバオフでも通信できました。
この場合シグナリングサーバとSTUNサーバしか使っていないので、シグナリング月5万回までは制限なしで使えます。最終的にはここも自分で実装予定ですが。

いきなり問題発生

やけに画質が悪い。
配信ソフトのプレビューは綺麗だし、仮想カメラを他の配信ソフトに入力して見ても綺麗だったので、ブラウザ側の問題と認定。

どうも、デフォルトの帯域がそこまで広くなく、それを超えるものはブラウザ側で自動的に圧縮され、画質が悪くなるようで....恐らく1.5Mbps程度で調整されていました。環境次第?
ブラウザのデフォルトにも、おそらくSkyway SDKのデフォルトにも制限はあるんじゃないかな。
FPS維持優先で、画質が先に悪くなる印象。

1. 入力ソースの解像度はちゃんと指定しないと圧縮される

getUserMediaでは幅、高さ、フレームレートを指定可能で、理想値を指定したり、最低値最大値の指定ができます。
何も書かず指定だけすると、理想値(ideal)と見なされます。
正直、理想値を下回る分には問題なさそうなので自分の環境で出力し得る最大を入れればいいです。
ソースではとりあえずHD 60fpsで指定してます。
とりあえずこれでプレビューする分には高画質で見れるはずです。
(chrome://media-internals/ で見た感じ、OBS cameraやXsplitの仮想カメラ機能は30fpsまでらしいですが、メディアソースのFPSは60ですし見た目で明らかに60fps対応です。
chromeの画面キャプチャも60fpsは対応してます)

2. 送るデータの最大ビットレートも指定しないと圧縮される

プレビューは綺麗だけどいざ通信を開始して受信した映像を見ると相変わらずの画質でした。
今度は送信する際になにか制限が掛かっているはずです。
SDPで帯域を指定するには、今回はSkywayのコードを書き換える必要があったり....?
幸いオープンソースなので覗いたところ、javascriptのコードに直に書き込まず、htmlでオプションを追加するだけでした。コードでは14000kbpsにしてあります。まぁ14Mbpsですね。
一応最大ビットレートの指定らしいですが、挙動としては制限というよりもリクエストに過ぎないような気がします。
また、通信途中に帯域変える場合はSDPを弄るのかな? これはまた別の機会に。

コード

ありふれてますが一応、htmlのコード github
プレビューしやすくするためにオート再生、コントロールは初めからオン。
htmlやjavascriptにはまだ詳しくありませんが、Skywayのリファレンスを見ればほとんどの人が作れるかと。

こいつの問題は、通信しだしてからのメディア変更に対応できないのを無理やり再接続で対応させているということ。
SFU通信の場合などはreplaceStream()が使えるみたいなんですがP2Pだとよくわからず力業です。いろいろ試してみます。
また、共有停止にlocalStream.getVideoTracks()[0].enabled = false; を使うとあくまでミュートでしかなく、元々HD60fpsで送信していた場合、HD60fpsの黒い画面を引き続き送るためビットレート的にはほぼ停止できますが、開封負荷により負荷は下がらない。
ちゃんと停止させる場合はlocalStream.getVideoTracks()[0].stop(); でOK。
今後変わる可能性は大いにあり。
画面共有のほうは共有停止ボタンが出るのでそれ押しても停止できます。

感想

現在欧州にいますが、東京の友人にTime isを共有して遅延を確認したところ、2秒未満でした
だいぶすごい。しかも電話が同じくらい遅延しているので会話している時は遅延0の感覚です(笑)
加えて、分かりやすく画質方向での改善が大きい。
まあ世の中の画面共有全部これにしたら輻輳の危険性が高まりますがね....
自分も友人もマシンパワーやネットワーク速度を考慮しないで済む環境なのでそこは楽でした。
CMAF chunkによる低遅延HLSでもなんでもそうですが、基本的に低遅延と負荷はトレードオフなのでそれなりにCPU負荷使いますね。特に双方向共有を一つのPCで横に並べて行うと顕著です。
次回はSkyway SDKなしでやってみる感じです。
その後はWebRTCというかP2Pでの画面共有についてもう少し詰めていきます。敢えて遅延があるほうが話しやすい場合もあるので、接続時に遅延を選択できたりすると面白いかも?(受信側でバッファする感じでしょうか)
API周りの理解が皆無で根拠が薄いので、仕様と異なる場合はぜひコメントしてください!

参考

便利な機能 chrome://webrtc-internals (ローカルリソースなのでリンクは組み込めません)送信ビットレートとかTURNサーバを経由しているどうかなど見れます
Skyway https://webrtc.ecl.ntt.com/
Skyway リファレンス https://webrtc.ecl.ntt.com/api-reference/javascript.html

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

jsGridの使い方について

jsGridとは

jsGridは、jQueryに基づく軽量のクライアント側データグリッドコントロールです。挿入、フィルタリング、編集、削除、ページング、ソートなどの基本的なグリッド操作をサポートしています。jsGridは柔軟で、その外観とコンポーネントをカスタマイズできます(http://js-grid.com/)。

jsGridができること

公式から一部紹介します。
- フィルタリング
- データ編集(レコードの追加、更新、削除)
- ページング
- 並び替え

環境構築

今回はCDNを使います。
一つのファイルにまとめていますが、複数ファイルに分けても問題ありません。

ファイルの中身

まず、初めにcdnリンクを記載していきます。

index.html
<html>
  <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jsgrid/1.5.3/jsgrid.min.css" />
  <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jsgrid/1.5.3/jsgrid-theme.min.css" />
  <script src="https://code.jquery.com/jquery-3.0.0.min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jsgrid/1.5.3/jsgrid.min.js"></script>
  <!--ここから下にこの後、記載します-->
</html>

次に、jsGridを適用させたい箇所を指定します

<div id="jsGrid"></div>

最後に、グリッドを作成します

<script>
    var clients = [
        { "Name": "Otto Clay", "Age": 25, "Country": 1, "Address": "Ap #897-1459 Quam Avenue", "Married": false },
        { "Name": "Connor Johnston", "Age": 45, "Country": 2, "Address": "Ap #370-4647 Dis Av.", "Married": true },
        { "Name": "Lacey Hess", "Age": 29, "Country": 3, "Address": "Ap #365-8835 Integer St.", "Married": false },
        { "Name": "Timothy Henson", "Age": 56, "Country": 1, "Address": "911-5143 Luctus Ave", "Married": true },
        { "Name": "Ramona Benton", "Age": 32, "Country": 3, "Address": "Ap #614-689 Vehicula Street", "Married": false }
    ];

    var countries = [
        { Name: "", Id: 0 },
        { Name: "United States", Id: 1 },
        { Name: "Canada", Id: 2 },
        { Name: "United Kingdom", Id: 3 }
    ];

    $("#jsGrid").jsGrid({
        width: "100%",
        height: "400px",

        inserting: true,
        editing: true,
        sorting: true,
        paging: true,

        data: clients,

        fields: [
            { name: "Name", type: "text", width: 150, validate: "required" },
            { name: "Age", type: "number", width: 50 },
            { name: "Address", type: "text", width: 200 },
            { name: "Country", type: "select", items: countries, valueField: "Id", textField: "Name" },
            { name: "Married", type: "checkbox", title: "Is Married", sorting: false },
            { type: "control" }
        ]
    });
</script>

出力結果

下記のようなグリッドが表示されます。
スクリーンショット 2020-08-10 15.18.51.png
今回は表示するデータをあらかじめ用意していますが、動的なデータを表示することも可能です。

参考リンク

公式:http://js-grid.com/

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

Reactを使用してWeb画面を作成する

はじめに

reactとreact-reduxを調べたときに概念や考え方など難しい話から入っているサイトやちょっと難しい(カッコいい)Web画面をサンプルに使用していて理解が難しいなと感じました。
そのため、単純なサンプルを使用して最低限の説明のみをしようと思います。

環境

  • node.js: v12.18.2
  • webpack: 4.44.1
  • React: 16.13.1

環境作成

node.jsのインストール

公式のサイトに従ってインストールしてください
公式サイト: https://nodejs.org/ja/

プロジェクト用のファイルを作成

node.jsのプロジェクトではプロジェクトの設定やインストールしたパッケージなどをpackage.jsonに記載します。
次のコマンドでpackage.jsonを作成するといくつかの入力項目がありますが基本的にすべてデフォルトで問題ないです。

npm init

Babel

環境やブラウザのバージョンによって使用できるJavaScriptの仕様が異なります。その仕様の差分を埋めるために、Babelを使用して作成したJavaScriptを対応可能なものに変換します。

Babelのインストール

npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-react @babel/register

Babelの設定作成

Babelの設定はプロジェクト直下の.babelrc に記載します。そのため、このファイルを作成して以下の内容を記載します。

{
    "presets": ["@babel/env", "@babel/preset-react"]
}

webpack

Web画面からJavaScriptを読みだす際にJavaScriptのファイルが多いと無駄な時間や処理が発生します。webpackを使用すると複数のファイルを一つにまとめていい感じにしてくれます。

webpackのインストール

webpackに必要なライブラリの他にもローカルでサーバを起動するために webpack-dev-server をインストールします。

npm install --save-dev webpack webpack-cli webpack-dev-server style-loader css-loader babel-loader

webpackの設定作成

webpackの設定はプロジェクト直下のwebpack.config.js に記載します。このファイルを作成して以下の内容を記載します。

webpack.config.js
const path = require("path");
const webpack = require("webpack");

module.exports = {
  entry: "./src/index.js",
  mode: "development",
  module: {
    // ファイルをどのように変換すればよいのかのルールを設定。
    // testで入力するファイルの条件、excludeで除外する条件、
    //loaderで外部ライブラリのルールを参照する 
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        loader: "babel-loader",
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      }
    ]
  },
  // ビルドの順番を設定
  resolve: { extensions: ["*", ".js", ".jsx"] },
  // ビルド後の設定
  // pathは、ビルド後のファイルを吐き出すフォルダ、
  // filenameはビルド後のファイル名を設定
  output: {
    path: path.resolve(__dirname, "dist/"),
    filename: "bundle.js"
  },
  // ローカルで起動するサーバの設定
  // contentBaseでブラウザからアクセスしたときのルート、
  // portはブラウザからアクセスするときのポート番号、
  // hotOnlyはファイルを更新したときに自動読み込みをする設定
  devServer: {
    contentBase: path.join(__dirname, "public/"),
    port: 8080,
    hotOnly: true
  },
  plugins: [new webpack.HotModuleReplacementPlugin()]
};

react

reactのインストール

npm install react react-dom

Web画面のソースの作成

フォルダの作成

プロジェクトルート直下にsrcとpublicとdistのフォルダを作成してください。
※上のwebpackの設定を変えたときはここも変えてください。

project_root
├─dist   // ビルド後のファイルを格納 
├─public // htmlを格納
├─src    // reactのJavaScriptファイルやCSSファイルを格納
├─.babelrc
├─package.json
├─webpack.config.js

htmlファイルの作成

ブラウザからアクセスした際に一番最初にアクセスされるhtmlファイルを作成します。
※webpackのビルド後のファイルをインポートするのを忘れないでください。

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>React Sample</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./bundle.js"></script>
  </body>
</html>

reactのrendarファイルの作成

reactの機能をつかってレンダリングするJavaScriptファイルを作成します。
ReactDOM.render()にコンポーネントファイルとdocument.getElementById(置き換えるhtmlのid)を指定してあげます。
上のindex.htmlの<div id="app"></div>とAppコンポーネントを置き換えたいのでReactDOM.render()<App />document.getElementById('app')を指定します。

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

ReactDOM.render(
  <App />,
  document.getElementById('app')
);

reactのコンポーネントファイルの作成

実際にWeb画面に表示するための情報を書いたコンポーネントのJavaScriptファイルを作成します。
基本的にはコンポーネントファイルを増やして、Web画面を増やしたりWeb画面の要素を増やしたりします。
※上のindex.jsのimport App from './App';でこのファイルをインポートしています。最後の行のexportを忘れないでください。

App.js
import React, { Component} from "react";

class App extends Component{
  render(){
    return(
      <div className="App">
        <h1> Hello, World! </h1>
      </div>
    );
  }
}

export default App;

Web画面の起動

ビルド

開発用のサーバを起動するときに同時にビルドが走るので特に必須ではないですが、webpackの設定や作成したファイルが間違っていないかをチェックするために一旦ビルドします。
プロジェクトのルートで次のコマンドを実行するとビルドが走ります。Windowsの場合はダブルクォーテーションで囲まないとうまくいかないです。

"./node_modules/.bin/webpack" 

ビルドが成功するとdictフォルダ内にJavaScriptファイルが一つできるはずです。

開発用サーバの起動

プロジェクトのルートで次のコマンドを実行するとビルドとサーバの起動が走ります。Windowsの場合はダブルクォーテーションで囲まないとうまくいかないです。

"./node_modules/.bin/webpack-dev-server"

サーバが起動したらブラウザからlocalhost:8080にアクセスするとHello, World!が表示されます。

終わりに

Reduxやaxiosとの連携を書こうと考えていましたが、予想以上に長くなったので今回はここまでにします。
次回以降にReduxとaxiosのサンプルと簡単な説明を書いていこうと思います。

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

配列内にあるオブジェクトを取り出して、スタイル要素に適用する。

やること

配列の中にあるオブジェクトの色要素を取り出して、表示させる文字の色にオブジェクトの色要素を割り当てるプログラムです。

ソースコード

全体のソースコードは以下の通りになります。(headタグやvueの読み込み箇所は省きます。)

HTMLファイルのソースコード

        <div id="app">
            <ul>
                <li v-for="(addColor,index) in colors">
                    <span :style="colors[index]">
                        {{addColor.color}}
                    </span>
                </li>
            </ul>
        </div>

jsファイルのソースコード

var app = new Vue({
    el: "#app",
    data: {
        colors: [
            {
                color: "red",
            },
            {
                color: "blue",
            },
            {
                color: "green",
            },
        ],
    },
});

ソースコードの解説

コードを書く順番で見ていきます。

色データの作成

data: {
        colors: [
            {
                color: "red",
            },
            {
                color: "blue",
            },
            {
                color: "green",
            },
        ],
    },

配列の中に3つのオブジェクトがある状態です。
具体的に説明するとdataの中にcolorsという名前の配列があり、その配列の中のオブジェクトにcolorというキーの名前と文字列が入っています。

配列の中身を一つずつ取り出して、表示させている。(反復処理、リストレンダリング)

<!-- (オブジェクト,インデックス番号) -->
<li v-for="(addColor,index) in colors"></li>

colors配列の中から、オブジェクトとインデックス番号を引数として取り出しています。

オブジェクト内にある文字列を取り出して、スタイルで色を割り当てる。

<span :style="colors[index]">
    {{addColor.color}} <!-- オブジェクト内になる文字列を表示 -->
</span>

vuestyle属性の操作を行うため、v-bindをつけます。
そのstyle属性の中で、配列の中身をインデックス番号で指定することによって、指定した先にあるオブジェクトを取り出しています。その結果、表示させている文字色に合わせて色のスタイルが割り当てらるようになります。
Qiita 2回目.JPG

最後に

基礎のv-ifやv-forだけでも、簡単なことから複雑なことまでたくさんのことができるので、これからどんどん挑戦していきたいと思います。

最後まで読んでいただきありがとうございました。
この記事が少しでもあなたのお役に立てば幸いです。

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

初学者がVue→Vuexの橋を渡ってみた

0.Vuex

logo.png
Vueを学習しているとVuexというものに出会う。「なんとなく難しそう...」ってので避けてたけど、学習してみることに。

初学者の自分にとって多少目新しい概念だったので、一旦記事にして整理しようと思います。

概念ベースでまとめたので文字が多めです。

1.まずVueの復習

Vuexに入る前にVueについて軽く復習したい。

Vueのメインの特徴といえば、"コンポーネント"の概念。

機能毎に部品に区切り、それを1つの.vueファイルとして扱う。

フッターのコンポーネント、サイドバーのコンポーネント、もっと細かくいくとボタンのコンポーネントなんてのも定義できる。

vueimages.001.jpeg

さらにこれらのコンポーネントはプロジェクト内で繰り返し使えるので非常に便利な機能となる。

このように便利な機能をもつVueだが、アプリケーションが大規模になってくるとちょっとした問題が出てくる。

2.Vueの弱点:コンポーネントが増えた時にどうなる?

Vueアプリケーションが大規模になってくると「異なるコンポーネントで別の状態を管理したい」と言った状況になることがしばしば発生する。

具体例以下のような状況が考えられる。

ECサイトの構築を行なっていて、ECサイト内のカートの実装を考えているものとする。ここで、カート内の商品の個数が変化する時はどういう時が考えられるだろうか??

・「カートから削除」を押す
・「購入決定」を押す

上記の様な状況が考えられる。この2つの状況を同じコンポーネント内で管理するのは難しいかと思われる。なのでコンポーネントを分けることになるだろう。そうするとコンポーネント間でのデータのやり取りが必要になる。

ただコンポーネント間のデータのやり取りを増やす事はあまり得策ではない。

・単純にコンポーネントへの記述書が増える
・子⇄親の双方向の受け渡しを迫られがち(結果、コードの可読性が下がる)

じゃあどうする??

3.ようやくVuexの出番

上記の問題を解決するのが、まさに「Vuex」。
つまり、コンポーネント毎にデータのやりとりを行うのではなく、プロジェクト内に各コンポーネントで共通に使うデータの置き場所を1箇所定め、各コンポーネントはそのデータの置き場を参照する。
vueimages.002.jpeg

こうすることでいちいち親子で値の受け渡しをすることもなく、データの源泉がより鮮明になる。

この共通データの置き場の概念をVuexではstoreと呼ぶ。

4.Vuexがどのように解決してくれるか

具体的にどのようにしてVuexがデータの管理や変更を行うのかVuexの代表的な概念である「state」「getter」「mutation」を説明しながらVuexの挙動を見ていきたいと思う。

4-1.state

Vuexにおけるデータの置き場。Vueにおける"data"に近い概念。
ただVueの"data"と違ってVuexの"state"はどのコンポーネントからもアクセスできる。

4-2.getter

stateを参照して、stateの値をちょろっと書き換えたものを出力したい時などに使用する。Vueにおける"computed"に近い概念。
ただし、getterはstateの値を書き換えることはできない。(重要)

4-3.mutation

stateを変更、更新する際に用いる。Vueにて、メソッドを定義してdataの値を変更する操作に近いイメージ。
Vuexにおいて、stateの値を変更するのは原則としてこのmutationしか行わない。

--

こう見るとVuexとVueって結構似てますね。図にすると下見ないなイメージでしょうか。

vueimages.003.jpeg


storeの値を書き換えるには、mutationにコミットするしかないの、不便じゃね??

って最初直感的に思った。でもどうやらそうでもないっぽい。

それは、制約を外してどこからでもstateを変更できるようにすると、後々の開発で「どこからstateが変更されたか」を追うのが大変になるから。

プログラムの世界では「ある機能に一定の制約をあえて設けることで、その機能の役割を明確化させる」みたいな仕組みにたまーに遭遇するけど、今回もその1例かなと。

この辺は実際に大規模アプリの開発とかに携わったりして経験を積まないとなかなか見えないところなのかもしれない。

まとめ:Vuexをうまく組み込んでベストな設計を築こう。

概念はなんとなく掴めたけど、結局使いこなせなければ意味がない。

次はVuexを使った具体的な設計パターンを学んでいって、より効率的な開発をVue.jsで行えるようにしていきたいと思う。

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

Vue.jsで広告を埋め込んだコンポーネントを作る

はじめに

エンジニア歴4年目に突入したハヤシと申します。(インフラ:2年,WEB:1年)
現在、前勤めていた会社の先輩と一緒に「Pokeloop」というアプリを作っており、そろそろ広告埋め込みたいなぁーと思ってVueで広告コンポーネントをつくってみました。google adsense等、大手の広告をvueに埋め込むやり方は結構出てくるけど、あまり有名じゃないところだと埋め込む方法が出てこなかったので備忘録的に残したいと思います!

ちなみに「pokeloop」はパーティー相性表をはじめとするポケモン対戦における便利なツールを提供しているサイトです!UI等かなりこだわってますので、ぜひ一度訪れてください!
https://pokeloop.com/

開発環境

vue: 2.6.10

扱う広告の種類

今回はjsを埋め込むと、自動的にDOMが生成されるタイプの広告を扱います。それ以外ではこのやり方ではうまく行かない場合があるかもしれません。

■動作確認広告
1.忍者AdMax
2.アスタ

作り方

自動的にDOMが生成されるタイプの広告をvueで扱うためには、iframeという他のサイトを埋め込めるhtmlのタグを使用して、広告を埋め込みます。単純にjsを埋め込むだけだとダメでした。

OKな例

iframeタグを作成して、その中にjsタグを埋め込む形にしています。

ads.vue
<template>
 <div ref="ads" class="ad"></div>
</template>

<script>
export default {
  async mounted() {
    const iframe = document.createElement('iframe');
    const head = document.getElementsByTagName('head')[0];
    this.$refs.ads.appendChild(iframe);
    const html = '<body><script src="https://cdn.com/somescript.js"><\/script><\/body>';
    const iframeDocument = iframe.contentWindow.document;
    iframeDocument.open();
    iframeDocument.write(html);
    iframeDocument.close();
  }
}
</script>

ダメな例

これではうまくいきません。多分document.writeでDOMを作ってる部分が動かないためです。

ads.vue
<template>
 <div ref="ads" class="ad"></div>
</template>

<script>
export default {
  async mounted() {
    // スクリプトタグを生成
    let scriptEl = document.createElement('script');
   // スクリプトタグにjsをセットする
    scriptEl.setAttribute('src', 'https://cdn.com/somescript.js');
    // this.$refsを使い、DOMに埋め込む
    this.$refs.ads.appendChild(scriptEl);
  }
}
</script>

<style lang="scss" scoped>
</style>

終わりに

いかがでしたでしょうか。これで広告コンポーネントを作成することができると思います。

なにか記事の内容に不備があればご指摘お願いします。

見ていただきありがとうございました!

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

GAS as Web App - XHRは使えるのか

発端

最近よくGoogle Apps Scriptの開発を行います。その中で頻度が増えているのが静的HTMLをGoogle Apps Scriptより返して簡易なWebホスティングを行うシーンです。当初はこんな構成を取りたいと考えました。

スクリーンショット 2020-08-10 13.48.28.png

データを返すApp Scriptを分ける意味

開発効率が良いと思った為です。HTMLを返すApp Script上でもgoogle.script.runを使えばHTML側からApp Scriptの関数を操作、データの取得が可能です。しかし幾つか開発をしていく中で以下のような不満にあたりました。

  • 開発・検証時に一々App Scriptにデプロイしなければ検証出来ない
  • 他プラットフォームへの転用時にコードの修正箇所が多い
  • 複数のApp Scriptから1つのシートを利用したい時、ファイルの管理が煩雑

これがクライアントサイドから直接別のApp Scriptよりデータを取得する仕組みが取れれば大変楽ができる、と思った次第です。

結論

先にどうなったか記載致します。最終的に限定的には出来る、でした。

  1. クライアントサイドからFetch or XHRで認証のかかっていないApp Scriptは実行出来る
  2. クライアントサイドからFetch or XHRで認証のかかっているApp Scriptは実行出来ない

詳細

以下実験してみた結果を記載します。

調査1

  • 普通にFetchしたらどうなる?

出来ました。ただし、Google Apps ScriptにはFetchの機能が限定的にしか使えず設定を工夫する必要がありました。

実験したコード

// データを取得するApp Script URLをセット
const endpoint = "https://script.google.com/macros/s/*********************/exec";
const xhr = new XMLHttpRequest();

xhr.onload = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    } else if (xhr.readyState === 4 && xhr.status !== 200) {
        console.log("Error");
    }
}

xhr.open("GET", endpoint, true);
xhr.responseType = "text";
xhr.send(null);

※ 前提:呼ばれる側のApp Scriptの権限は全公開

限定的だったこと

  • FetchでPreflightが発生すると失敗する
  • GAS as Web Appは別App Script宛でもCORS Policyに引っかかる

GASの環境下ではPreflightリクエストをする際のOptionに対応していないようでした。この為、以下MDNに記述されているうち単純リクエストにて対応しなければ送付できませんでした。

MDN CORS

また、GASは以下Developerサイトに記載されている通りiframe内にて実行されています。この為どうしてもクロスドメイン扱いとなってしまいました。

HTML Service: Restrictions

調査2

  • 認証情報を渡したらどうなる?

調査1からクロスドメインになってしまう事がわかったので、全公開していないApp Scriptにもアクセスするべくソースを以下の通りに書き換えました。withCredentialsを付け加えてCookieを持たせることを明示的に示した形です。

const endpoint = "https://script.google.com/macros/s/*********************/exec";
const xhr = new XMLHttpRequest();

xhr.onload = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    } else if (xhr.readyState === 4 && xhr.status !== 200) {
        console.log("Error");
    }
}

xhr.open("GET", endpoint, true);
xhr.withCredentials = true; // 追加
xhr.responseType = "text";
xhr.send(null);

これに対するErrorは以下の通りです。

Access to XMLHttpRequest at 'https://script.google.com/macros/s/XXXX/exec' from origin 'https://XXXX-0lu-script.googleusercontent.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

withCredentialsを指定する際にはAPI側のレスポンスはAccess-Control-Allow-Origin:*であってはならず明示的にOriginを指定せよ、という事のようです。これはクライアント側ではどうしようもないと思いますので次の調査を行いました。

調査3

  • App ScriptよりResponse HeaderのAccess-Control-Allow-Originの設定値を変える事は出来るか。

Google Apps Scriptの開発者向けサイトを諸々調べましたがこれの実現方法がありません。私が見落としているだけかもしれませんがこれは全く見つかりませんでした。

結果

ページ上部にも記載した通りですが以下の通り結果となりました。元のSpreadSheetの情報は公開したくない、見られたくないデータであるので、今回の調査結果としては達成できそうにありませんでした。

  • Fetchする際に認証としてCookieを渡そうとすると失敗する

  • 認証さえ不要であれば問題なくアクセス可能

その他アイディア

上記が出来ないなら以下があるじゃないか、と言われると思いますので記載しておきます。

  • App Script APIで関数実行する
  • JSONPでやる
  • Sheet APIでApp Scriptを経由せずにデータを取得する

今回は上記3点ともセキュリティの問題か用途合わずで使えなかったのですが、こういったやり方もあると思います。

以上、読んで頂きましてありがとうございました。
もしこれなら出来るよ!というやり方があれば是非教えて頂けるとありがたいです。

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

js で Fizz Buzz

Fizz Buzz も知らないプログラマーなんて... みたいな記事を読んだ。

知らんかったので、やる。

Fizz Buzz ?

最初のプレイヤーは「1」と数字を発言する。次のプレイヤーは直前のプレイヤーの次の数字を発言していく。ただし、3で割り切れる場合は「Fizz」(Bizz Buzzの場合は「Bizz」)、5で割り切れる場合は「Buzz」、両者で割り切れる場合(すなわち15で割り切れる場合)は「Fizz Buzz」(Bizz Buzzの場合は「Bizz Buzz」)を数の代わりに発言しなければならない。発言を間違えた者や、ためらった者は脱落となる。

このゲームをコンピュータ画面に表示させるプログラムとして作成させることで、コードが書けないプログラマ志願者を見分ける手法をJeff AtwoodがFizzBuzz問題 (FizzBuzz Question) として提唱した。

出典: Fizz Buzz - Wikipedia

なるほど。

JavaScript で実装

❶ 配列を作るパターン

  • オーソドックスに「連番の配列」を用意して評価。
function fizzBuzz(max) {
  return Array(max).fill().map((val, idx) => {
    const num = idx + 1;
    if (num % 15 === 0) return 'FizzBuzz';
    if (num % 3 === 0) return 'Fizz';
    if (num % 5 === 0) return 'Buzz';
    return num;
  });
}

console.log(fizzBuzz(30));

// 結果:
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz", 16, 17, "Fizz", 19, "Buzz", "Fizz", 22, 23, "Fizz", "Buzz", 26, "Fizz", 28, 29, "FizzBuzz"]

(補足) 連番配列の作り方は、どれがいい?

計測した中では、Array(arrayLength).fill().map((val, idx) => idx) が良さそう。

/**
 * 関数を指定回数実行し、平均実行時間を取得する
 * 
 * @param max 試行回数
 * @param func 実行する関数
 * @returns 平均実行時間 (ms)
 */
function getAvgRunTime(max, func) {
    let count = 0
    let timeArr = [];
    while (count < max) {
        let start = performance.now();
        let result = func();
        let end = performance.now();
        timeArr.push(end - start);
        count++;
    }
    return timeArr.reduce((acc, cur) => acc + cur) / timeArr.length;
}

// いろいろな「連番の配列を作る方法」を各 30 回実行し、それぞれの平均実行時間を出力
console.log(getAvgRunTime(30, () => Array(1000000).fill().map((val, idx) => idx) ));
console.log(getAvgRunTime(30, () => [...Array(1000000)].map((val, idx) => idx) ));
console.log(getAvgRunTime(30, () => Array.from(Array(1000000).keys()) ));
console.log(getAvgRunTime(30, () => [...Array(1000000).keys()] ));
console.log(getAvgRunTime(30, () => Array.from({length: 1000000}, (val, idx) => idx) ));

// apply 関数を使う方法は、生成する要素数が多いと Maximum call stack size exceeded などで失敗するので割愛
// console.log(getAvgRunTime(30, () => Array.apply(null, Array(1000000)).map((val, idx) => idx) ));
連番配列の生成方法 Chrome v84 Firefox v79
Array(1000000).fill().map((val, idx) => idx) 19 ms ? 19 ms
[...Array(1000000)].map((val, idx) => idx) 19 ms ? 22 ms
Array.from(Array(1000000).keys()) 28 ms 14 ms
[...Array(1000000).keys()] 38 ms 13 ms ?
Array.from({length: 1000000}, (val, idx) => idx) 53 ms 15 ms

❷ ジェネレーター関数を使うパターン

  • メモリを無駄に使わないように。
function getFizzBuzzAnswer(num) {
    if (num % 15 === 0) return 'FizzBuzz';
    if (num % 3 === 0) return 'Fizz';
    if (num % 5 === 0) return 'Buzz';
    return num;
}

function* makeFizzBuzzIterator(start = 1, end = Infinity) {
    let num = start;
    while (num < end) {
        yield getFizzBuzzAnswer(num);
        num++;
    }
    return getFizzBuzzAnswer(num);
}

let iterator = makeFizzBuzzIterator(1, 30);

while (true) {
    let result = iterator.next();
    console.log(result.value);
    if (result.done) break;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.js + GitHub + Travis CI + Code ClimateでCI入門

はじめに

この記事は、JavaScript開発でCI環境を導入するためのガイドです。
もしCI環境の導入に興味を持ったら、この記事を土台にみなさまの環境にあったCI/CDへ発展させてください。

対象となる読者

  • JavaScriptで開発をしている
  • CIという言葉を聞いたことがある
  • テスティングフレームワークを使ったことがない
  • バグ修正をしたら別の箇所でバグが出た
  • 依存パッケージの更新作業に負担を感じる

この記事は、テスト自動化やCIに興味はあるが、まだ導入したことがない開発者を対象としています。

対象とする環境

  • node.js 12.18.3
  • jest 26.0

CIとは / テスト自動化とは

CI (Continuous Integration / 継続的インテグレーション)とは、短期間で開発ブランチを統合し続ける開発手法です。グループ開発では、開発者がそれぞれ作業ブランチを抱えます。その作業ブランチは、定期的に統合しないと細分化し続けます。細分化しすぎたブランチは統合に膨大な作業が必要になるインテグレーション地獄を引き起こします。このインテグレーション地獄を回避するために発案された手法がCIです。

テスト自動化とは、ソフトウェアによってテストの設計、実行、報告を支援する取り組みです。テスト自動化に必要なソフトウェアをセットにしたものがテスティングフレームワークです。CIにおけるテストは、作業ブランチが正常に統合できているかを判定します。

テスト自動化はCIを実現するために必須の要素です。CIにおけるテストツールは、開発ブランチが統合されるたびにテストを自動実行し結果を報告し続けます。CIに必要な回数のテストを手動で実行するのは、現実的ではありません。

テスト自動化はCIの実施に必須の要素です。そのためCI環境には必ずテストツールが含まれます。

テスト自動化の利点

テスト自動化には、以下のような利点があります。

  • リグレッションの防止
  • 複数環境での動作確認
  • CD(継続的デリバリー / 継続的デプロイメント)への発展

リグレッションの防止

プログラムを修正すると、思わぬ箇所に影響しバグを発生させてしまう危険性があります。修正により取り去ったはずのバグが再発したり、他の機能が正常に働かなくなることをリグレッションデグレードといいます。

テスト自動化はリグレッションの発生を早期に発見し、問題箇所を特定するのに役立ちます。その結果、新機能開発や機能修正の負担を減らします。

プログラムが依存しているnpmモジュールの更新も、リグレッションやデグレードを引き起こします。依存関係が複雑なほどモジュール更新によるリグレッションの発見は難しくなります。テスト自動化はこうした問題の発見と修正作業の負担を減らします。

複数環境での動作確認

複数のプラットフォームをサポートするプログラムの品質を維持するのは大変な作業です。さらにnode.jsの複数のバージョンをサポートすると、テストの工数は掛け合わせで増えていきます。

テストツールは、動作環境の構築を自動化します。そのためテスト工数を圧縮できます。

CD(継続的デリバリー / 継続的デプロイ)への発展

CI環境は、CD(継続的デリバリー / 継続的デプロイメント)へ発展できます。

継続的デリバリーは、テストを通じでソフトウェアを本番環境へ展開可能か判定する開発手法です。継続的デプロイメントは、テストに合格したコードを自動的に本番環境へ展開する開発手法です。CDはソフトウェア開発と運用を一体化し、ソフトウェアの更新頻度を向上させます。

CI環境の構成

本記事では、以下のサービスを組み合わせてCI環境を構築します。

  • リモートリポジトリ : GitHub
  • テスティングフレームワーク : jest
  • CIサービス : Travis CI
  • コードカバレッジ収集 : Quality By Code Climate

jest

jestはJavaScriptテスティングフレームワークです。テストファイルの作成、実行、テスト網羅率のレポートまでを1つのフレームワークでカバーします。今回はjestを最小限の構成で導入する方法をご紹介します。

インストール

Getting Started

最初に、jestをインストールします。

npm install --save-dev jest

テストファイルを書く

つぎにテストファイルを作成します。

プロジェクトルートにsum.jsというJavaScriptファイルがあり、その中にsumという関数があるとします。同じディレクトリにtest.spec.jsを作成し、sum関数をテストします。

sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
test.spec.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

sum関数に引数1,2を与え、戻り値が3であればテスト成功、それ以外ならテスト失敗となります。

テストの実行

最後に、package.jsonにテスト実行コマンドを追加します。

package.json
{
  "scripts": {
    "test": "jest"
  }
}

npm run testでコマンドを実行すれば、テストが実行されて結果が報告されます。

より詳しい導入方法は、こちらの記事を参照してください。

Travis CI

Travis CIとは、リポジトリの監視、テストの実行、結果の報告を行うCIサービスです。さまざまな言語 / OS / テスティングフレームワークに対応しており、今回はnode.jsとjestの組み合わせでテストを行います。

Travis CIはバプリックリポジトリでは無料で利用できます。プライベートリポジトリのテストは有料サービスとなります。

サインイン

まずはサインインページからGitHubアカウントでサインインします。

リポジトリの追加

つぎにトップページのプラスボタンを押して、テスト対象リポジトリを追加します。

パブリックリポジトリの一覧が表示されるので、チェックボックスをONにします。

これでTravis CIがリポジトリの監視を開始します。Travis CIはmasterブランチにコードがpushされると動き出します。その都度テストが実行され、結果がGitHubアカウントのメールアドレスに報告されます。

設定ファイル

Travis CIはリポジトリのルートディレクトリに配置された.travis.ymlという名前の設定ファイルにしたがって動作します。node.jsでテストを実行するには、以下の設定ファイルを追加します。

travis.yml
language: node_js
node_js:
  - "10"
  - "12"
  - "14"

language: node_jsを設定すれば、Travis CIがpackage.jsonを探してtestコマンドを実行します。node_jsの中にバージョン番号を追加すれば、Travis CIはそれぞれのnode.jsでテストを並列実行します。

.travis.ymlのより詳しい記述方法は、公式ドキュメントを参照してください。

ymlファイルに関する詳しい解説は、こちらの記事をご参照ください。

テストの実行

.travis.ymlをリモートリポジトリのmasterブランチにpushすれば、Travis CIがテストを開始します。

Travis CIは

  • 仮想マシンの起動
  • 言語環境のインストール
  • package-lock.jsonyarn.lockなどにしたがって依存モジュールをインストール
  • package.jsonのtestスクリプトを実行
  • テスト結果を報告

という手順でテストを実行します。

CIサービスは、テストの実行のたびに初期状態の仮想マシンを起動し、モジュールをインストールします。そのためローカルのテストでは発見しにくい、動作環境に依存するバグも発見できます。

すべてのテストをパスすれば、ビルドの状態を表すバッジの色が緑になります。

Quality By Code Climate

Travis CIには、テストがどれだけのコードを網羅しているかを表すコードカバレッジを表示する機能がありません。コードカバレッジが低すぎると、リグレッションの発生を見逃す可能性が高くなります。

Quality By Code Climateは、コード品質を監視、維持するためのオンラインサービスです。今回はこのサービスをTravis CIと連携させ、コードカバレッジを監視します。

Quality By Code Climateも、パブリックリポジトリでは無料で利用できます。

サインイン

Code Climateの「LOGIN」メニューからQUALITYサービスにログインします。

「Open Source」を選択し、サービスを開始します。

リポジトリの追加

つぎに、監視するリポジトリを追加します。「Add a repository」ボタンを押して、リストからパブリックリポジトリを選びます。

リポジトリの初回スキャンが始まり、しばらくするとコードの監視が始まります。

Test Report IDの取得

初期状態では、Test Coverageの欄が雨傘アイコンになっています。これはコードカバレッジの情報がないことを表します。

Travis CIとの連携には、Test Report IDが必要になります。下のスクリーンショットを参考に、IDを取得してください。

このIDはCode Climateにコードカバレッジ情報を送信する書き込み専用キーです。パブリックリポジトリに公開しても問題ありません。(公式ドキュメント

Travis CIとの連携

testコマンドに、カバレッジ出力オプションを追加します。

package.json
{
  "scripts": {
    "test": "jest --coverage"
                  ^^^^^^^^^^
  }
}

Test Report IDをTravis CIに渡すため、.travis.ymlを書き換えます。

travis.yml
env:
  global:
    - CC_TEST_REPORTER_ID=【ここにTest Report ID】
language: node_js
node_js:
  - "10"
  - "12"
  - "14"
before_script:
  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
  - chmod +x ./cc-test-reporter
  - ./cc-test-reporter before-build
after_script:
  - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT

参考 CodeClimate : Travis CI Test Coverage

Test Report IDを環境変数にセットします。テスト前に報告用アプリケーションをダウンロード、テスト後にCode Climateへ結果を送信します。

このpackage.json.travis.ymlをmasterブランチにプッシュします。

確認

Quality By Code Climateのページへ戻り、雨傘アイコンを確認します。
雨傘アイコンがパーセンテージに変更されていれば、無事に設定完了です。

個人的な感想

GitHub Actionsの登場によりCI/CD環境の選択肢が大きく広がりました。CI環境を小さく導入することで、こうした動きをより理解しやすくなります。

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

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

VS CodeでESlint、Prettierを使用したReact環境を構築する

Reactの環境構築はcreate-react-appでとても簡単になりました。ここにコードチェック、整形ツールであるESlint、Prettier を導入してみます。

ESlint、Prettierを使用した環境構築方法やルールはプロジェクトによって違うと思うので、基本的な設定だけしています。プロジェクトに合わせて編集してください。

完成品はGitHubにアップしています。create-react-appで生成されるReactのアイコンなどは削除しています。

https://github.com/nineharker/react-vscode-eslint-prettier

環境構築

それでは環境構築していきましょう!パッケージ管理にyarnを使っていきますが、npmを使用している人は便宜読み替えてください。

VS CodeにESlintとPrettierの拡張機能を追加する

VS Codeの拡張機能としてESlintをインストールしましょう。
eslint-800x358.png

Prettierもインストールします。
prettier-800x358.png
これで必要な拡張機能はインストールできました。

create-react-appでプロジェクトを作成する

Reactプロジェクトを作成しましょう。

create-react-app sample

必要なパッケージをインストールする

create-react-appで作成された雛形では、すでにESLintに関するパッケージが導入されています。

create-react-appで作成したプロジェクトの場合、eslintとbabel-eslint、eslint-loaderをインストールしたらエラーが発生するのでインストールしないください。

yarn add --dev prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-react

eslint、prettierの設定ファイルを生成する

プロジェクトルートに.eslintrc.jsと.prettierrc配置してルールを書いていきます。 基本的な設定だけを書いています。

.eslintrc.js
module.exports = {
  "env": {
    "es6": true,
    "node": true
  },
  "parser": "babel-eslint",
  "plugins": [
    "react",
    "prettier"
  ],
  "parserOptions": {
    "version": 2018,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:prettier/recommended",
    "prettier/react"
  ],
  "rules": {
    "prettier/prettier": "error"
  }
}
.prettierrc
{
  "printWidth": 120,
  "useTabs": false,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "es5",
  "bracketSpacing": true,
  "jsxBracketSameLine": false
}

VS Codeの設定でセーブ時に整形するようにする

セーブ時に整形するようにVS Codeの設定を変更しましょう。VS Codeのデフォルトのフォーマット機能をオフにしています。

 {
   "javascript.format.enable": false,
   "eslint.autoFixOnSave": true
 }

おわり

お疲れ様でした!これで設定が完了です。App.jsなどのコンポーネントの拡張子はjsxに変更しましょう。

ESlint、Prettierの設定は大変ですが、その後の開発が圧倒的に楽になるのでぜひお試しください。

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

Webフロントツール:「webpack」と「npm-scriptsによる個別ライブラリの実行」の比較

1.webpack:Webフロントのバンドルを含むタスクランナープラットフォーム

(1) 概要

 webpackは、以下の処理を実行できるオールインワンツール。

  • Javascriptのビルド、バンドル。これが基本。
  • プラグインの利用により、SASSのビルド、バンドル、画像の最適化、HTMLの静的ジェネレートなど。

(2) webpackのメリット

  • これを導入すればWebのフロントに関するリソースのビルド、バンドル、作成、最適化が一括でできる。
  • 副次的・事後的な効用として、webpackを利用することで、Webフロントですべき処理が何かを知ることができる。いわば、情報のindexのような役割もある。これは、webpackそのものというより、それらを取り巻く情報の効用だと思う。
  • Javacriptに限らず、SASS、画像、HTMLなど異なるリソースに関するビルド、バンドル、最適化の処理を、webpackのconfigに則して記載できる。つまり、共通のルールで異なるリソースのビルドの設定ができる。学習がしやすい。

(3) webpackのデメリット

webpackはデファクトスタンダードなので、Webのフロント開発をする人は1度は経験しておくべきものであるが、設定が増えた場合に問題点も見えてくる。

  • 基本はJavascriptのバンドルであり、その他のものはPluginを通して、副次的に処理をする。つまり、JS以外の設定は、少し変則的で、Pluginクラスのコンストラクタに設定することになる。例えば、SASSだけをビルドするにしても、途中経過としてJSファイルができてしまい、それを削除するプラグインを入れたりする。
  • Javascript以外は、Pluginを必ず導入する必要があり、Pluginはライブラリのラッパーであることが多く、ライブラリとPluginの2つをインストールするという手間が発生することがある。
  • Javascript以外のリソースの設定を増やしていくと、webpack.config.jsの記載が増えて見にくくなる。(Javascriptなので、分割して書けば見やすくはなる。)
  • タスクを実行するライブラリとPluginが異なるため、webpack自体や、ラッパーのもととなるライブラリとのバージョン不整合などで動かなくなることがある。修正版がすぐに出ない場合、独自でPluginを記載して対応などをする必要がある。
  • 便利なライブラリがあってもPluginを探す、または、作らないと行けない。(Pluginは簡単につくれるが)

webpackに限らないが、色々な処理をしていくことで、設定ファイルが複雑になり、分けて書いたほうが分かりやすいのではと思ってくる。
 であれば、最初からPluginがラッピングしている元のライブラリを個別で管理して、npm-scriptsで実行しても良いのではないかという気もする。
 

2.npm-scriptsで個別のライブラリを実行

(1) webpackの性質と問題点

 webpackは一つのリソースで完結するので便利であるが、処理することが増えてくると、webpack.config.jsが肥大化してくる。
 では、configを分けて記載するという方法もあるが、それであれば最初から、webpackを介さずに、個別に設定して、npm-scriptsで書けば、webpack、pluginを入れなくてもよいので、タスクランナーのレイヤーが一つ少なくなる
 結局は、webpackは、タスクランナープラットフォームであり、それ自体であらゆるビルド、バンドル、最適化をするわけではない。強いて言えば、Javascriptのバンドラーという側面が強い。(例えば、SASSの書き出し先の指定は、jsとは異なり、Pluginのコンストラクタで指定したりする。)

(2) npm-scripts + 個別ライブラリを利用するメリット

npm-scriptsと個別ライブラリによりビルド、バンドル、最適化を実行する場合のメリットは以下。

  • npmはNode.jsを使う以上は必ず存在する。それに備わっているタスク実行の仕組みを使えば、追加的なライブラリのインストールが減る。(タスクランナーのレイヤーが一つ減る)
  • 個別のライブラリごとに設定をすると、webpackのPluginを介さないので、直接、設定情報だけを設定すればよく、設定ファイルが見やすい
  • 個々のライブラリのバージョンアップは、非同期で進むが、独立して管理するので、全体が動かないということが少なくなる。一つのライブラリの変化が他のライブラリーの実行に影響しにくい

(3) npm-scripts + 個別ライブラリを利用するデメリット

webpackと比較すると、npm-scripts + 個別ライブラリにした場合、以下のデメリットが考えられる。

  • 設定ファイルが分かれるため、管理リソースが増える。(小さい場合、webpackの方がリソースが少ない)。
  • 設定ファイル個別のルールや書式で記載しなければならず、学習コストが高い。(webpackはPluginを使うにしても、全体の設定は統一的。)
  • Javascript、CSS、SASS、画像などのビルド、バンドル、最適化に関するライブラリの情報を探すのがwebpackより集めにくい。(webpackは、結果論として関連する情報が調べやすい)

3.フロントのビルドツールの選択基準

 webpackとnpm-scripts+個別ライブラリを比較して、それぞれのメリットとデメリットを記載した。
 それらを踏まえてツールを利用する際に、以下のような選択基準や導入方法が良いのではないだろうか。

  • 初心者はwebpack。
  • ビルド、バンドル、最適化の処理が少ないのであればwebpackが設定ファイルが少なく、手間も少なくて済む。
  • 設定が増え、複雑になるのであれば、npm-scripts+個別ライブラリの方が分けて管理するので分かりやすく、変化に耐えやすい。
  • npm-scriptsを使うにしても、どのような処理やライブラリが必要かをwebpackというキーワードで調べる。その方が効率的に情報が集まる。
  • npm-scriptsは基本ワンライナーでの設定の方が見やすいが、複数ビルドなど追加的な処理がある場合は、Javascriptで間接的にライブラリを実行するファイルをつくる。(JavascriptAPIというのが多くのライブラリのマニュアルに記載がある)

備考

 npm-scriptsと個別のビルドやバンドルツールを使う場合、2020年8月時点で良さそうだなと思うのが以下(他にもいろいろあるだろうが)。
 これらをnpm-scriptsでそれぞれ設定し、npm-run-allで、watchオプションとして実行する。これで、webpack-dev-serverと同じ様なことができる。
 サーバの起動などについていえば、Lieve Serverの方が分かりやすい気もする。

  • node-sass:Sassのビルド
  • 11ty:HTMLの静的サイトジェネレーター(テンプレートはpug、nunjacksなど色々選べる)
  • rollup.js:JSのビルド、バンドラー(JSのみのライブラリのバンドルなどでよく利用される)
  • Live Server:確認用のサーバ。ドキュメントルートのリソースに変更があれば、自動でブラウザをリロードしてくれる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

連想配列(Map)のメンバとよく使う操作【JavaScript】

はじめに

連想配列は、ObjectではなくMapを使ったほうが良い(こともある)という記事を読み、積極的に使うことにしました。

参考:

Mapを使うにあたり、「こうしたいときどうするんだっけ?」となった時に参考にできるものが欲しかったので、自分で記事にまとめることにしました。

この記事について

以下について書いています

  • Mapのメンバ
  • Mapでよく使う操作

以下については書いていません

  • そもそも配列とは何か
  • Object配列との違い

連想配列とは何か

連想配列とは・・・

添え字にスカラー数値以外のデータ型(文字列型等)も使用できる配列である。

連想配列は、配列の値(value)に名前(key)をつけて管理することができます。

// Arrayだと名前はつけられない
const arrayTestResults = new Array(
  77,
  91,
  82
)

// Mapだと名前をつけられる
const mapTestResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

参考:

配列の宣言

宣言だけする

const map = new Map()

宣言と同時に初期値を設定する

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

メンバ

一覧

名称 説明
.size 要素数を取得する
.has(key) keyが一致する要素の有無を真偽値で取得する
.get(key) keyが一致する要素の値を取得する
.set(key,value) keyが一致する要素の値を上書き(または追加)する
.delete(key) keyが一致する要素を削除する
.clear() 全ての要素を削除する
.keys() 全ての要素のkeyを取得する
.values() 全ての要素のvalueを取得する
.entries() 全ての要素のkeyとvalueを取得する
.forEach(callback[, thisArg]) 引数のcallbackに渡した処理をそれぞれの要素に実行する

.size

要素数を取得します。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
  ['理科', 55],
  ['地理', 60],
  ['歴史', 49],
]);

const size = testResults.size
// 6

*プロパティなのでカッコ()が不要です。
size()ではなくsizeと書きます。

.has(key)

keyが一致する要素の有無を真偽値で取得します

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

const hasEnglishResult = testResults.has('英語');
// true

const hasHistoryResult = testResults.has('理科');
// false

.get(key)

keyが一致する要素の値を取得します。
keyが一致する要素がなかった場合はundifinedを取得します。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

const englishResult = testResults.get('英語');
// 82

const historyResult = testResults.get('理科');
// undefined

.set(key,value)

keyが一致する要素の値を上書きします。
keyが一致する要素が存在しない場合、新たに要素を追加します。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

// 上書き
testResults.set('英語', 100);
// Map {
//   '国語' => 77,
//   '数学' => 91,
//   '英語' => 100,
// }

// 追加
testResults.set('理科', 55);
// Map {
//   '国語' => 77,
//   '数学' => 91,
//   '英語' => 100,
//   '理科' => 55,
// }

.delete(key)

keyが一致する要素を削除します。
削除ができたかどうかを有無を真偽値で取得することもできます。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

console.log(testResults.delete('数学'));
// true
// Map {
//   '国語' => 77,
//   '英語' => 82,
// }

console.log(testResults.delete('理科'));
// false
// Map {
//   '国語' => 77,
//   '英語' => 82,
// }

.clear()

全ての要素を削除します。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
  ['理科', 55],
  ['地理', 60],
  ['歴史', 49],
]);

console.log(testResults.clear());
// Map {}

.keys()

全ての要素のkeyを取得します。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

const keys = testResults.keys();
// [Map Iterator] {
//   '国語',
//   '数学',
//   '英語'
// }

*実行結果はシンボルという特殊な型で出力されます。
配列として扱う方法は次章「よく使う操作」で説明します。

.values()

全ての要素のvalueを取得します。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

const values = testResults.values();
// [Map Iterator] {
//   77,
//   91,
//   82
// }

*実行結果はシンボルという特殊な型で出力されます。
配列として扱う方法は次章「よく使う操作」で説明します。。

.entries()

全ての要素のkeyとvalueを取得します。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

const entries = testResults.entries();
// [Map Entries] {
//   ['国語', 77],
//   ['数学', 91],
//   ['英語', 82]
// }

*実行結果はシンボルという特殊な型で出力されます。
配列として扱う方法は次章「よく使う操作」で説明します。

.forEach(callback[, thisArg])

引数のcallbackに渡した処理を要素それぞれの要素に実行します。
Array.prototype.forEach()と同じような動きをします。

// forEach()
const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

testResults.forEach((value, key, map) => {
  console.log(value, key, map);
});
// 77 国語 Map { '国語' => 77, '数学' => 91, '英語' => 82 }
// 91 数学 Map { '国語' => 77, '数学' => 91, '英語' => 82 }
// 82 英語 Map { '国語' => 77, '数学' => 91, '英語' => 82 }

よく使う操作

Iteratorを配列として扱う

keys()やvalues()の実行結果はシンボルという特殊な型で出力されます。
配列ではないので、配列用の関数を実行するとエラーになります。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

const keys = testResults.keys();
// [Map Iterator] {
//   '国語',
//   '数学',
//   '英語',
// }

keys.forEach((key) => console.log(key));
// TypeError: keys.forEach is not a function

参考:

方法1. Array.from()

配列として値を取得したい場合は、Array.from()を使って変換すればOKです。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

const keys = Array.from(testResults.keys());
// [
//   '国語',
//   '数学',
//   '英語'
// ];

keys.forEach((key) => console.log(key));
// 国語
// 数学
// 英語

方法2. 分割代入([...配列])

分割代入[...Map.keys()]を利用して配列にすることも可能です。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
]);

const keys = [...testResults.keys()];
// [
//   '国語',
//   '数学',
//   '英語'
// ];

keys.forEach((key) => console.log(key));
// 国語;
// 数学;
// 英語;

キーでソートする

数値のkeyを数の若い順に並び替えます

const testResults = new Map([
  [6, '歴史'],
  [1, '国語'],
  [4, '理科'],
  [5, '地理'],
  [2, '数学'],
  [3, '英語'],
]);

const sortedTestResults = new Map(
  [...testResults.entries()].sort((a, b) => a[0] - b[0])
);
// Map {
//   1 => '国語',
//   2 => '数学',
//   3 => '英語',
//   4 => '理科',
//   5 => '地理',
//   6 => '歴史'
// }

参考:
Stack Overflow -Is it possible to sort a ES6 map object?-

最大値を取得する

値の中で最大の数値を取得します。

const testResults = new Map([
  ['国語', 77],
  ['数学', 91],
  ['英語', 82],
  ['理科', 55],
  ['地理', 60],
  ['歴史', 49],
]);

const maxNum = Math.max(...Array.from(testResults.values()));
// 91

重複する値の数をカウントする

配列内の同じ値の出現回数を要素ごとにカウントします。

const animals = ['いぬ', 'ねこ', 'いぬ', 'ねこ', 'いぬ', 'いぬ'];
const animalCounts = new Map();

for (let animal of animals) {

  // 一致するkeyがあればカウントアップ
  if (animalCounts.has(animal)) {
    const count = animalCounts.get(animal);
    animalCounts.set(animal, count + 1);

    // 一致するkeyがなければ新たに値をset
  } else {
    animalCounts.set(animal, 1);
  }
}

console.log(animalCounts);
// Map {
//   'いぬ' => 4,
//   'ねこ' => 2,
// }

まとめ

メンバは直感的にわかるものばかりなので覚えやすいのですが、配列への変換操作にはまだ慣れません。。
ネットで調べようとしても、Array.prototype.map()とかObjectの連想配列が混ざっててなかなか目的のものが手に入らないんですよね。
ググラビリティの問題かもですが、、

覚えるまでは、この記事をみながら使おうと思います。

参考

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

診察予約システム(LINE×GAS)にプッシュメッセージ機能を追加

概要

耳鼻科の開業医をしています。自院の予約システムをGASを使ったLINE Botで作成しました。
1時間で出来る LINE×GASで順番取り予約システムの作成
LINE×GASで作成した順番取り予約LINE Botを改良

今まで使用していた業者さんの予約システムは解約してしまったので、自作の予約システムの使い勝手を良くしていくしかありません。

診療を継続できないような緊急事態(医師が救急搬送に付き添うとか、医師の体調が悪くなるとか、停電とか)では、予約した患者さんが来院されても診療を受けることが出来ません。このような時に診察予約済みの患者さんに「現在来院されても診療できないので連絡ください」のようなプッシュメッセージを送る機能を追加しました。

今回実装した機能

1.緊急事態には予約した患者さんにプッシュメッセージを送る
2.スタッフはLINEで受付時間中でも予約券の発券を停止できる
3.待ち時間が短い時は発券できない(予約者が順番に遅れることが多いので)
4.予約券に来院時間の目安を提示(予約者が順番に遅れないようにするため)

概念図

バックエンドとしてGoogle Spread Sheetを利用しApp Script(GAS)でLINE botと連携。作成法はこちら 1時間で出来る LINE×GASで順番取り予約システムの作成
「LINE Bot+APIで表現してアウトプット」 概念図.png

Spread SheetのA1セルが発券済み番号、B1セルが診察中番号、C1セルが「1」の時に発券停止、D1セルが一人当たりの待ち時間(分)、E列に予約券を発行した患者さんのLINE userIDが記載されるようにしました。

image.png

機能を追加

1.Google Spread SheetのE列の空いてるセルにユーザーIDを登録する関数recordLineUserId(userId)を作成

function recordLineUserId(userId) {
  var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  // E列の空いているセルの行番号を取得する。(E1,E2が既に埋まっていたらnext=3となる)
  var next = activeSheet.getRange("E:E").getValues().filter(String).length + 1;
  Logger.log(next);
  // E列の空いてるセルにユーザーIDを登録する
  activeSheet.getRange(next, 5).setValue(userId);
};

2.取得したLINEuserIdにプッシュメッセージを送る関数sendPushMessages()を作成

function sendPushMessages() {
  // E列のLINE userIdを取得する
  var userIdList = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange("E:E").getValues().filter(String).flat();

  // 重複を削除する(1ユーザー1プッシュメッセージしか送らないようにする)
  userIdList = Array.from(new Set(userIdList));
  Logger.log(userIdList);

  // プッシュメッセージを送信する
  for (var userId of userIdList) {
    Logger.log(userId);

    UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", {
      headers: {
        "Content-Type": "application/json; charset=UTF-8",
        Authorization: "Bearer " + ACCESS_TOKEN,
      },
      method: "post",
      payload: JSON.stringify({
        to: userId,
        messages: [
          {
            type: "text",
            text: "このメッセージは医院で何らかの緊急事態が発生し、診療できない状況時にお送りするものです。本日未来院の方はご来院頂いても診察できない可能性がありますので医院にお問い合わせ下さい。",
          },
        ],
      }),
    });
  }
}

3.C1セル値を取得する関数getNumberC1()を作成、getNumberD1()も同様に作成

function getNumberC1() {
  //1. 現在のスプレッドシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  //2. 現在のシートを取得
  var sheet = spreadsheet.getActiveSheet();
  //3. 指定するセルの範囲(C1)を取得
  var range = sheet.getRange("C1");
  //4. 値を取得する
  var value = range.getValue();
  //ログに出力
  return value;
}

4.C1セルを「1」にする関数stopC1()を作成

function stopC1() {
  //1. 現在のスプレッドシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  //2. 現在のシートを取得
  var sheet = spreadsheet.getActiveSheet();
  //3. 指定するセルの範囲(C1)を取得
  var rangeC = sheet.getRange("C1");
  //4. C1セル値を1にする:セルに値をセットする場合はsetValueを使う
  rangeC.setValue(1);
}

5.待ち時間(分)を取得する関数getTime()を作成

function getTime() {
  //1. 現在のスプレッドシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  //2. 現在のシートを取得
  var sheet = spreadsheet.getActiveSheet();
  //3A. 指定するセルの範囲(A1)を取得
  var rangeA = sheet.getRange("A1");
  //3B. 指定するセルの範囲(B1)を取得
  var rangeB = sheet.getRange("B1");
  //4A. 値を取得する
  var valueA = rangeA.getValue();
  //4B. 値を取得する
  var valueB = rangeB.getValue();
  //待ち人数
  var number = valueA - valueB - 1;
  //待ち時間
  var time = number * getNumberD1();
  //ログに出力
  return time;
}

6.ifで分岐させる

//患者さんが[リッチメニューの予約券発行]をタップ
if (userMessage === "発券") {
  //休診日(日曜または土曜午後)
  if (day === 0 || (day === 6 && 1400 <= now)) {      
      messages[0].text = "土曜午後・日曜・祝日は休診日です";    
    //予約時間内 
    } else if ((830 <= now && now < 1100) || (1400 <= now && now < 1630)) {
     //C1セルが「1」の時は発券停止
      if (getNumberC1() === 1) {        
        messages[0].text = "現在発券が停止されています。医院にお問い合わせください。";
      } else {
        //待ち時間が12分以下なら発券しない
        if (getTime() <= 12) {
          messages[0].text =
            "現在待ち時間が12分以下のため予約券は発券できません。直接ご来院ください。";
        } else {
          //userIdを取得
          recordLineUserId(event.source.userId);
          // フレックスメッセージ(予約券)
          messages = getReservedTicket();
        }
      }
    } else {
      messages[0].text =
        "現在発券時間外です。受付時間は午前8:30~11:00 午後2:00~4:30です。";
    }
  }

7.スタッフがLINEであるメッセージを送ると、発券が停止され、予約患者にプッシュメッセージが送られる

  //スタッフが「stop(仮)」をLINEに送ると発券停止  
  if (userMessage === "stop") {
    //C1セルが1に変更
    stopC1();   
    messages[0].text = "発券(患者用)を停止しました。";
  //スタッフが「push(仮)」をLINEに送るとプッシュメッセージ
  } else if (userMessage === "push") {
    sendPushMessages();
    messages[0].text = "プッシュメッセージを予約患者に送りました。";
  }

プッシュメーセージを送信できました。
image.png

8.getTime()を使い来院時間を表示

function getReservedTicket() {
  return [
    {
      type: "flex",
      altText: "**耳鼻咽喉科 予約券",
      contents: {
        type: "bubble",
        body: {
          type: "box",
          layout: "vertical",
          contents: [
            {
              type: "text",
              text: "**耳鼻咽喉科 診察予約券",
              weight: "bold",
              color: "#1DB446",
              size: "sm",
              align: "center",
            },
            {
              type: "text",
              text: String(getNumber()),
              weight: "bold",
              size: "5xl",
              margin: "xxl",
              align: "center",
            },
            {
              type: "separator",
              margin: "xxl",
            },
            {
              type: "box",
              layout: "vertical",
              margin: "xxl",
              spacing: "sm",
              contents: [
                {
                  type: "box",
                  layout: "horizontal",
                  contents: [
                    {
                      type: "text",
                      text: "・こちらの画面を受付でご提示下さい",
                      size: "sm",
                      color: "#555555",
                      flex: 0,
                    },
                  ],
                },
                {
                  type: "box",
                  layout: "horizontal",
                  contents: [
                    {
                      type: "text",
                      text: "・遅れた場合予約券は無効になります",
                      size: "sm",
                      color: "#555555",
                      flex: 0,
                    },
                  ],
                },
                {
                  type: "box",
                  layout: "horizontal",
                  contents: [
                    {
                      type: "text",
                      text: "・こまめに【待ち状況】をご確認下さい",
                      size: "sm",
                      color: "#555555",
                      flex: 0,
                    },
                  ],
                },
              ],
            },
            {
              type: "separator",
              margin: "xxl",
            },
            {
              type: "box",
              layout: "horizontal",
              margin: "md",
              contents: [
                {
                  type: "text",
                  text: "来院時間",
                  size: "sm",
                  color: "#4169e1",
                  flex: 0,
                },
                {
                  type: "text",
                  text: "発券から" + String(getTime() - 3) + "分後まで",
                  color: "#4169e1",
                  size: "sm",
                  align: "end",
                },
              ],
            },
          ],
        },
        styles: {
          footer: {
            separator: true,
          },
        },
      },
    },
  ];
};

image.png

考察

今回で元々考えていた機能は実装できました。無料でここまで使えるものが自作できるとは自分でも驚いています。今後はスタッフや患者さんの意見を聞きながら改良を続けていきたいです

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