- 投稿日:2020-08-10T23:35:38+09:00
ストップウォッチ作ってみた(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.jsstart.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);
- 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.jsstart.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.jsstop.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.jsstart.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.jsreset.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.cssbody { 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 でも作りたい
- 投稿日:2020-08-10T22:43:30+09:00
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.jsimport { 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.jsimport { configureStore } from '@reduxjs/toolkit'; import messageReducer from './slice/messageSlice'; export default configureStore({ reducer: { message: messageReducer, }, });storeをコンポーネントに適用させる
コンポーネント間で情報をやり取りするために先ほど作成したstoreをreactのrenderに登録します。
index.jsimport 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.jsimport 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.jsReactの実行
前回同様に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.jsimport { 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.jsimport 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画面や保持する情報が増えたら明確にメリットがわかるようになると思います。
- 投稿日:2020-08-10T22:24:52+09:00
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]; // BlueUint32Arrayは、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]); // RedUint32Arrayをつかった方法と見比べて見ましょう。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(); })();関連
- 投稿日:2020-08-10T22:20:37+09:00
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の基礎の基礎を書かせていただきました。
初心者が自分用に書いておりますので何かの間違いがあるかもしれません。その時はアドバイスしていただけると幸いです!!
- 投稿日:2020-08-10T22:01:36+09:00
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">
- 投稿日:2020-08-10T21:49:51+09:00
Express + MongoDBで作成したAPIサーバーをJestでテストする
はじめに
テストを書くのは好きですか?
アプリケーションを作成するうえで、テストは必ず書かなきゃいけないものですが、テストの書き方まで丁寧に記載されている書籍やサイトって少ないように感じます。
今回はExpress + MongoDBで作成したAPIサーバーをJestでテストします。
まずはモジュールごとに依存関係を切り離した単体テストを作成します。
Jestの強力なモック機能を活用することができます。その後supertestを用いた結合テストまで作成します。
supertestは擬似的なHTTPリクエストを送ることができます。テスト対象のアプリケーション
予め作成しておいた以下のコードをテストします。
機能としては簡単に、/api/users/:username
をGET
で叩くと指定したユーザーネームのユーザーを取得でき、/api/users
をPOST
で叩くと、ユーザーを作成できるという最小限のものです。すべてのコードはここから確認することができます。
https://github.com/azukiazusa1/express-testMVCモデルにのっとり、大きくわけて
ルーティング
、コントローラー
、モデル
で構成されています。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.tsimport 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 approutes/index.ts
src/routes/index.tsimport Express from 'express' import userRoutes from './userRoutes' // すべてのルーティングをここにまとめます。 const router = Express.Router() router.use('/users', userRoutes) export default routerroutes/userRoute.ts
src/routes/userRoute.tsimport Express from 'express' import usersController from '../controllers/usersController' const router = Express.Router() router.get('/:username`, userController.show) router.post('/', usersController.create) export default routercontrollers/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.tsimport 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.jsmodule.exports = { presets: [ ['@babel/preset-env', {targets: {node: 'current'}}], '@babel/preset-typescript', ], };jest.config.js
Jestではデフォルトのテスト環境はブラウザになるので、Node.jsの環境でテストするように設定を追加します。
プロジェクトルートに
jest.config.js
を追加します。jest.config.jsmodule.exports = { testEnvironment: 'node' }テストのテスト
テストのテストを書いて確認してみましょう。
test
フォルダを作成し、その中にindex.spec.ts
ファイルを作成し、簡単なテストを記述します。test/index.spec.tsdescribe('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.tsimport { 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.tsjest.mock('../../src/Models/user', () => ({ create: jest.fn((user: User) => { const _id = '12345' return Promise.resolve({ _id, ...user }) }) }))モデルをモック化し、本来エクスポートされた
User
モデルが持っているcreate
メソッドはモック関数として受け取ったオブジェクトに_id
プロジェクトを足して返すという単純なものになっています。これで、他のモジュールとの依存を切り離し、テストを書くことができます。
test/controllers/userController.spec.tsimport { 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.tslet 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.tsdescribe('異常系', () => { 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.jsmodule.exports = { ++ preset: '@shelf/jest-mongodb', // testEnvironmentは競合するので削除 -- testEnvironment: 'node' }jest-mongodb-config.js
jest-mongodb-config.js
を作成して、以下の設定を記述します。
設定可能なすべてのオプションは、こちらを参照してください。jest-mongodb-config.jsmodule.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.tsimport 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') }) }) }) })ポイントは、
beforeAll
、beforeEach
、afterAll
でデータベースに関する準備を行っているところです。順番に見ていきましょう。beforeAll
beforeAll
は、describe
ブロックの中でテストを実施する前に一度だけ呼び出されます。後述のbeforeEach
よりも先に呼び出されます。ここではメモリサーバーへの接続を行っています。
通常と異なる点として、mongoose
の接続先に(global as any).__MONGO_URI__
を指定しています。
__MONGO_URI__
はメモリサーバーの接続先を表しています。beforeEach
beforeEach
はdescribe
ブロックの中ですべてのテストごとに実施されます。
ここで一度すべてのデータを削除してからテストデータを挿入することによって、テスト間の依存が発生しないようにしています。
collection.insertMany()
はcreate()
よりも早く一括でデータを挿入することができますが、バリデーションは実施されないので注意が必要です。afterAll
afterAll
はdescribe
ブロックの中で最後に一度だけ呼び出されます。ここでデータベースとの接続を切っておかないとテストが正常に終了しないので忘れずにここの処理を書いておきましょう。
バリデーションテスト
バリデーションエラーが発生した場合、例外をスローするので、そのことをテストで確認します。
例外がスローされたかどうかは
expect().rejects.toThrow()
で確認します。test/Models/user.spec.tsimport 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.tsdescribe('バーチャルフィールド', () => { 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.tsif (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.tsif (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.tsimport 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にするために改良できる点がまだあるかと思います。
ぜひ試してみてください。
- 投稿日:2020-08-10T21:35:42+09:00
文字列化された関数を実行する
- 投稿日:2020-08-10T21:22:41+09:00
GAS で掲示板をこしらえる
きょうび、自鯖に掲示板を用意したいと思っても、CGI や PHP のプログラムを配布しているサイトは軒並み停止していて、スマホ対応なんて望むべくもなく、もはや自作するしかない……ということで、なぜか Gogle Apps Script でスレッド式掲示板を作ってみました。
そんで、出来上がったものがこちら
ソースコードは GitHub に上げました。
GAS-BBS
GitHub 使うの初めてなので、全ファイルサイト上でコピペしてリポジトリ作りました?かなり簡素な作りですので、利用したいという奇特な方は、いい感じに改造してからご利用ください。
苦労した点
ぶっちゃけあんまないけど、HTML テンプレート経由だと勝手にエスケープしちゃって br 要素を出力することもできないので、仕方なく本文を CSS で white-space:pre-line して改行が反映されるようにしています。
そんなことより全然使い込んでないので、後々問題が出てくる気はしますが、もはや若人には「え?ビービーエ…なんて?」とか言われちゃう掲示板を作りたい方はチャレンジしてみるのも良いかと思います。
- 投稿日:2020-08-10T21:04:22+09:00
引数に最小値(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」ページにて使用していますので
ぜひ動きをみてにきてください。
- 投稿日:2020-08-10T20:18:23+09:00
Vue.js向けの高機能・高性能なテーブルコンポーネントvxe-tableを紹介したい
何に関する記事か?
vxe-table というVue.js向けのテーブルUIコンポーネントを紹介する記事です。
かなり高機能・高性能なライブラリなのですが、中国発ということもあり日本語の情報が見当たらなかったので記事にしてみました。
この記事ではコンポーネントの提供する機能のほんの一部しか紹介していません。より詳細な情報は以下のリンクから参照してください。
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>
これで
vxe-table
のガイドを読むのに最低限必要な準備は完了です。
後は vxe-table 公式ガイド を読めば実現したい機能を実装していけるはずです!
vxe-table
と他のコンポーネントライブラリとの性能比較
vxe-table
ではvirtual scroller
が標準で組み込まれているため、大量データでも高い性能を発揮します。
個人的にこのライブラリで一番感動したポイントなので、大量データのテーブル実装を行ってみて、簡易的な性能比較をしてみたいと思います。具体的には、Element / Vuetify / vxe-table でそれぞれ10列×1000行のテーブルデータと行選択できるチェックボックスを実装します。
1. Element
https://jsfiddle.net/Nag729/f3j2txnm/20/
スクロールからすでに遅く、チェックボックスの選択はスムーズとは言い難いですね
ソートも同様に時間がかかっています。2. Vuetify
https://jsfiddle.net/Nag729/u5jgqvr4/2/
Element と比較すると優秀で、スクロールにストレスはありません
ただ、全データの選択やカラムソートになると結構待たされます3. vxe-table
https://jsfiddle.net/Nag729/o13xmpn0/6/
スクロール・チェックボックス選択・ソートの全てがスムーズに動いています
自前でライブラリや設定を追加しなくてもこれだけの大量データに対応してくれるのは嬉しいですね!終わりに
以上、vxe-table の紹介でした。
現状でもかなりの機能が用意されている上に、これから先もバージョンアップが予定されているようなので、是非一度使ってみてください。
- 投稿日:2020-08-10T20:00:06+09:00
コーダーが独学JavaScriptを駆使してHTMLメルマガの制作業務を自作ツールで駆逐する話
メルマガ業務駆逐ツールを制作中です
自動画像スライサー出来ました✨https://t.co/35kxWAcAKh
— じゃがいも - JS勉強中 (@haruka_develop) August 2, 2020
※Chrome以外動きません!(必要十分なので他ブラウザ対応の予定なし)
また、Macでしか検証してません。
いくつかのバグは追々なおすとして、決まった使い方をすれば直ぐにでも自分のメルマガ制作の助けになりそう\(^^)/ pic.twitter.com/J0ZRCX7aLo作ったツールはこちら: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※矩形選択範囲に関しては基本的にプチモンテさんで掲載しているコードで実装させていただきました
コードジェネレーターへの進化
軌道に乗り始めてきたところでアイディアが更に発展する。
メルマガを作る上で案件ごとに異なるのは、主に下記に列挙するものたち。(以降、これらを案件変数と呼びます)
- 挨拶文テキスト(大抵、デバイスフォントで作る)
- 画像ファイル、画像の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!
実装中です:コンテキストメニューを設置して人間が入力した指定データを保持する
- 投稿日:2020-08-10T19:36:31+09:00
JAVASCRIPT 開発者を採用する方法
JavaScript言語は、JavaおよびC言語に基づいた構成を持っている、強力なクライアント側のマルチパラダイムの動的言語です。それに多数のタイプとオペレーター、組み込みオブジェクトやメソッドが含まれています。JavaScriptはオブジェクト指向プログラミングと関数型プログラミングの両方をサポートしていますので、1つの言語内でほぼどんなオブジェクトまたは機能でも作成できます。
JavaScriptの起源と最新のトレンドに詳しいJavaScript プログラマーを採用する方法
JavaScriptは単独で実行不可能であり、JavaScriptコードを実行するブラウザーが存在します。 ユーザーがJavaScriptの有効なHTMLページを開こうとすると、スクリプトがブラウザーに送信され、ブラウザーはスクリプトの下で動作します。ブラウザー以外に、JavaScriptはAdobeサービス、サーバーサイド環境、データベース、SVG画像などで表示することもできます。JavaScript言語は幅広いタイプのアプリケーションに使用できます。
JavaScript開発の基本、利点やトラップ
2018年にリリースされたState of the Developer Ecosystemのレポートによると、JavaScriptは3年連続で、世界で最も使用されているプログラミング言語であると認識されました。この調査は、世界中の17か国の6千人のプログラマーを対象に実施されました。
JavaScriptプログラミング言語を使用し、他の言語の専門家の代わりにJavaScript プログラマーを採用する利点と欠点を説明します。間違いなく、利点の方が多いです。
Javascript プログラマー コスト
PayScaleによるトップ5か国のフリーランスベースでのJavaScript エンジニアの平均年間報酬をご覧ください。
その国の一般的なJavaScript プログラマーの時給を詳しく見てみると、1時間あたりの報酬はイギリスが一番高いことが明らかになります。それでも、ウクライナのフリーランスJavaScript 開発者の時給は、JavaScriptプログラマーが勤務ないしは居住する上位国の中で最低です。ウクライナからのJavaScriptプログラマーが技術的知識と創造力において、世界中で非常に人気であるということはよく知られています。したがって、トップクラスのフリーランスJavaScriptエンジニアを採用する予定がある場合は、ウクライナのコーダーの採用をご検討ください。
ウクライナの開発者にご興味のある方は、以下のリンクをご参照ください。
https://jp.mobilunity.com/blog/hire-javascript-developer-jp/
- 投稿日:2020-08-10T18:56:02+09:00
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無事、コミットできるようになりました
- 投稿日:2020-08-10T18:29:08+09:00
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)を作成できます。この環境は基本的に作成した人以外は使わないので、クローズドな環境で自分のペースで開発ができます。つまり、各自に開発環境が与えられるので開発環境がボトルネックになることはなく並行開発ができるようになるのです。
次に、単体テストが終わったものは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に置くだけで自動デプロイ環境ができるので、皆さんもぜひ試してみてください。それでは。
- 投稿日:2020-08-10T18:16:13+09:00
browser-syncでAPIをproxyする
始めに
ローカルでの開発でAPIのCORSを回避するためにproxyを通すことがあると思いますが、webpack-dev-serverを使うと簡単に設定することができます。また、検索にも引っ掛かりやすいのでつまづいたら調べることができます。
webpack.config.jsdevServer: { 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/#devserverproxyscript.jsconst 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
- 投稿日:2020-08-10T17:07:09+09:00
【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はユーザー一覧画面と各ユーザーの詳細画面が用意されているシンプルなアプリケーションです。
ユーザー一覧画面↓
ユーザー詳細画面↓
『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.htmlSPAの場合
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.htmlNuxtアプリケーションの挙動の違いについて
実際に起動させたアプリケーションを例に、挙動の違いについて紹介します。
SSGの場合
実際の画面がプレビューで表示されていることから分かるとおり、SSGではレンダリングされた画面がレスポンスとして返ってきます。
レスポンスデータにも具体的な値がすでに書き込まれています。
このように、SSGでは各画面の静的ファイルを事前に生成しておきレスポンスとして返します。
ユーザー情報なども静的な情報として書き込まれているため、SSGではデータの更新がある度に静的ファイルを再生成する必要があります。SPAの場合
実際の画面がプレビューに表示されていないことから分かるとおり、SPAでは画面の作成はフロントエンド(ブラウザ)で行っています。
SSGではレスポンスに具体的な値が書き込まれていましたが、SPAの場合はJavaScriptが組み込まれています。
ユーザー詳細画面も同様です。
このように、SPAではJavaScriptの埋め込まれた
index.html
をレスポンスとして返します。
フロントエンドでJavaScriptを読み込むことで画面を表示したり画面遷移を実現したりしています。まとめ
以上で具体例を用いたSSG・SPAの違いの紹介を終わります。
- 今回のまとめ
- SSGでは『nuxt generate』で各画面の静的ファイルが生成される
- SAPでは『nuxt generate』で生成される静的ファイルはルートのみ
- SSGでは静的ウェブサイトからのレスポンス時に画面が作成されている
- SPAでは画面の作成はブラウザ上で行われる
参考記事
- 投稿日:2020-08-10T16:55:06+09:00
アップロードされたファイルの拡張子とサイズをチェックする方法
- 環境
- 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
やりたいこと
- アップロードされたファイルの拡張子が指定のもの以外の場合はエラーにしたい
- アップロードされたファイルのサイズが指定より大きかった場合はエラーにしたい
- エラーメッセージは親画面で指定したい
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で親画面から取得する
- エラーメッセージを親画面の隠し項目で設定しておく
- 子画面の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. 子画面を表示するときにパラメータでメッセージを渡す
- 親画面で子画面を表示するJavaSctiptを生成するときにエラーメッセージをGETのパラメータで設定する
- 子画面を開いたらパラメータを
f:viewParam
で受け取ってバッキングビーンに設定する- バッキングビーンのエラーメッセージをJSON形式で置いておく
- 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. 親子画面で同じバッキングビーンを使う
- 親子画面で共通のバッキングビーンにエラーメッセージ取得処理を実装する
- あとは「方法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.javapackage 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; } }
- 投稿日:2020-08-10T15:50:40+09:00
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
- 投稿日:2020-08-10T15:40:58+09:00
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-10T15:39:50+09:00
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 initBabel
環境やブラウザのバージョンによって使用できるJavaScriptの仕様が異なります。その仕様の差分を埋めるために、Babelを使用して作成したJavaScriptを対応可能なものに変換します。
Babelのインストール
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-react @babel/registerBabelの設定作成
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-loaderwebpackの設定作成
webpackの設定はプロジェクト直下の
webpack.config.js
に記載します。このファイルを作成して以下の内容を記載します。webpack.config.jsconst 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-domWeb画面のソースの作成
フォルダの作成
プロジェクトルート直下にsrcとpublicとdistのフォルダを作成してください。
※上のwebpackの設定を変えたときはここも変えてください。project_root ├─dist // ビルド後のファイルを格納 ├─public // htmlを格納 ├─src // reactのJavaScriptファイルやCSSファイルを格納 ├─.babelrc ├─package.json ├─webpack.config.jshtmlファイルの作成
ブラウザからアクセスした際に一番最初にアクセスされる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.jsimport 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.jsimport 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のサンプルと簡単な説明を書いていこうと思います。
- 投稿日:2020-08-10T15:14:04+09:00
配列内にあるオブジェクトを取り出して、スタイル要素に適用する。
やること
配列の中にあるオブジェクトの色要素を取り出して、表示させる文字の色にオブジェクトの色要素を割り当てるプログラムです。
ソースコード
全体のソースコードは以下の通りになります。(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>
vue
でstyle
属性の操作を行うため、v-bind
をつけます。
そのstyle
属性の中で、配列の中身をインデックス番号で指定することによって、指定した先にあるオブジェクトを取り出しています。その結果、表示させている文字色に合わせて色のスタイルが割り当てらるようになります。
最後に
基礎のv-ifやv-forだけでも、簡単なことから複雑なことまでたくさんのことができるので、これからどんどん挑戦していきたいと思います。
最後まで読んでいただきありがとうございました。
この記事が少しでもあなたのお役に立てば幸いです。
- 投稿日:2020-08-10T14:37:30+09:00
初学者がVue→Vuexの橋を渡ってみた
0.Vuex
Vueを学習しているとVuexというものに出会う。「なんとなく難しそう...」ってので避けてたけど、学習してみることに。初学者の自分にとって多少目新しい概念だったので、一旦記事にして整理しようと思います。
概念ベースでまとめたので文字が多めです。
1.まずVueの復習
Vuexに入る前にVueについて軽く復習したい。
Vueのメインの特徴といえば、"コンポーネント"の概念。
機能毎に部品に区切り、それを1つの.vueファイルとして扱う。
フッターのコンポーネント、サイドバーのコンポーネント、もっと細かくいくとボタンのコンポーネントなんてのも定義できる。
さらにこれらのコンポーネントはプロジェクト内で繰り返し使えるので非常に便利な機能となる。
このように便利な機能をもつVueだが、アプリケーションが大規模になってくるとちょっとした問題が出てくる。
2.Vueの弱点:コンポーネントが増えた時にどうなる?
Vueアプリケーションが大規模になってくると「異なるコンポーネントで別の状態を管理したい」と言った状況になることがしばしば発生する。
具体例以下のような状況が考えられる。
ECサイトの構築を行なっていて、ECサイト内のカートの実装を考えているものとする。ここで、カート内の商品の個数が変化する時はどういう時が考えられるだろうか??
・「カートから削除」を押す
・「購入決定」を押す
上記の様な状況が考えられる。この2つの状況を同じコンポーネント内で管理するのは難しいかと思われる。なのでコンポーネントを分けることになるだろう。そうするとコンポーネント間でのデータのやり取りが必要になる。
ただコンポーネント間のデータのやり取りを増やす事はあまり得策ではない。
・単純にコンポーネントへの記述書が増える
・子⇄親の双方向の受け渡しを迫られがち(結果、コードの可読性が下がる)じゃあどうする??
3.ようやくVuexの出番
上記の問題を解決するのが、まさに「Vuex」。
つまり、コンポーネント毎にデータのやりとりを行うのではなく、プロジェクト内に各コンポーネントで共通に使うデータの置き場所を1箇所定め、各コンポーネントはそのデータの置き場を参照する。
こうすることでいちいち親子で値の受け渡しをすることもなく、データの源泉がより鮮明になる。
この共通データの置き場の概念を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って結構似てますね。図にすると下見ないなイメージでしょうか。
storeの値を書き換えるには、mutationにコミットするしかないの、不便じゃね??
って最初直感的に思った。でもどうやらそうでもないっぽい。
それは、制約を外してどこからでもstateを変更できるようにすると、後々の開発で「どこからstateが変更されたか」を追うのが大変になるから。
プログラムの世界では「ある機能に一定の制約をあえて設けることで、その機能の役割を明確化させる」みたいな仕組みにたまーに遭遇するけど、今回もその1例かなと。
この辺は実際に大規模アプリの開発とかに携わったりして経験を積まないとなかなか見えないところなのかもしれない。
まとめ:Vuexをうまく組み込んでベストな設計を築こう。
概念はなんとなく掴めたけど、結局使いこなせなければ意味がない。
次はVuexを使った具体的な設計パターンを学んでいって、より効率的な開発をVue.jsで行えるようにしていきたいと思う。
- 投稿日:2020-08-10T13:52:20+09:00
Vue.jsで広告を埋め込んだコンポーネントを作る
はじめに
エンジニア歴4年目に突入したハヤシと申します。(インフラ:2年,WEB:1年)
現在、前勤めていた会社の先輩と一緒に「Pokeloop」というアプリを作っており、そろそろ広告埋め込みたいなぁーと思ってVueで広告コンポーネントをつくってみました。google adsense等、大手の広告をvueに埋め込むやり方は結構出てくるけど、あまり有名じゃないところだと埋め込む方法が出てこなかったので備忘録的に残したいと思います!ちなみに「pokeloop」はパーティー相性表をはじめとするポケモン対戦における便利なツールを提供しているサイトです!UI等かなりこだわってますので、ぜひ一度訪れてください!
https://pokeloop.com/開発環境
vue: 2.6.10
扱う広告の種類
今回はjsを埋め込むと、自動的にDOMが生成されるタイプの広告を扱います。それ以外ではこのやり方ではうまく行かない場合があるかもしれません。
作り方
自動的に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>終わりに
いかがでしたでしょうか。これで広告コンポーネントを作成することができると思います。
なにか記事の内容に不備があればご指摘お願いします。
見ていただきありがとうございました!
- 投稿日:2020-08-10T13:50:52+09:00
GAS as Web App - XHRは使えるのか
発端
最近よくGoogle Apps Scriptの開発を行います。その中で頻度が増えているのが静的HTMLをGoogle Apps Scriptより返して簡易なWebホスティングを行うシーンです。当初はこんな構成を取りたいと考えました。
データを返すApp Scriptを分ける意味
開発効率が良いと思った為です。HTMLを返すApp Script上でも
google.script.run
を使えばHTML側からApp Scriptの関数を操作、データの取得が可能です。しかし幾つか開発をしていく中で以下のような不満にあたりました。
- 開発・検証時に一々App Scriptにデプロイしなければ検証出来ない
- 他プラットフォームへの転用時にコードの修正箇所が多い
- 複数のApp Scriptから1つのシートを利用したい時、ファイルの管理が煩雑
これがクライアントサイドから直接別のApp Scriptよりデータを取得する仕組みが取れれば大変楽ができる、と思った次第です。
結論
先にどうなったか記載致します。最終的に限定的には出来る、でした。
- クライアントサイドからFetch or XHRで認証のかかっていないApp Scriptは実行出来る
- クライアントサイドから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に記述されているうち単純リクエストにて対応しなければ送付できませんでした。
また、GASは以下Developerサイトに記載されている通りiframe内にて実行されています。この為どうしてもクロスドメイン扱いとなってしまいました。
調査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点ともセキュリティの問題か用途合わずで使えなかったのですが、こういったやり方もあると思います。
以上、読んで頂きましてありがとうございました。
もしこれなら出来るよ!というやり方があれば是非教えて頂けるとありがたいです。
- 投稿日:2020-08-10T12:27:39+09:00
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) として提唱した。
なるほど。
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; }
- 投稿日:2020-08-10T11:39:15+09:00
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を最小限の構成で導入する方法をご紹介します。
インストール
最初に、jestをインストールします。
npm install --save-dev jestテストファイルを書く
つぎにテストファイルを作成します。
プロジェクトルートに
sum.js
というJavaScriptファイルがあり、その中にsum
という関数があるとします。同じディレクトリにtest.spec.js
を作成し、sum関数をテストします。sum.jsfunction sum(a, b) { return a + b; } module.exports = sum;test.spec.jsconst 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.ymllanguage: 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.json
やyarn.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.ymlenv: 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環境を小さく導入することで、こうした動きをより理解しやすくなります。
以上、ありがとうございました。
- 投稿日:2020-08-10T11:37:59+09:00
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をインストールしましょう。
Prettierもインストールします。
これで必要な拡張機能はインストールできました。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.jsmodule.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の設定は大変ですが、その後の開発が圧倒的に楽になるのでぜひお試しください。
- 投稿日:2020-08-10T10:59:36+09:00
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:確認用のサーバ。ドキュメントルートのリソースに変更があれば、自動でブラウザをリロードしてくれる。
- 投稿日:2020-08-10T10:47:12+09:00
連想配列(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の連想配列が混ざっててなかなか目的のものが手に入らないんですよね。
ググラビリティの問題かもですが、、覚えるまでは、この記事をみながら使おうと思います。
参考
- 投稿日:2020-08-10T10:36:29+09:00
診察予約システム(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で順番取り予約システムの作成
Spread SheetのA1セルが発券済み番号、B1セルが診察中番号、C1セルが「1」の時に発券停止、D1セルが一人当たりの待ち時間(分)、E列に予約券を発行した患者さんのLINE userIDが記載されるようにしました。
機能を追加
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 = "プッシュメッセージを予約患者に送りました。"; }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, }, }, }, }, ]; };考察
今回で元々考えていた機能は実装できました。無料でここまで使えるものが自作できるとは自分でも驚いています。今後はスタッフや患者さんの意見を聞きながら改良を続けていきたいです