- 投稿日:2021-01-17T23:54:39+09:00
卒業研究でQiitaに自分のWebサービスを埋め込みたくなった
タイトルで落ちてる。
そもそも
Qiitaではiframe要素の埋め込みとか、javascriptの実行とかは基本的にできない。
この辺に関しては実験記事を参照。というわけで
ユーザースクリプトで実装することにした。
Qiitaとかnoteとかは勝手にhtml要素を入れたり、あと属性付与したりできない。
付与した場合は勝手に消される。リンクを張るのに必須のaタグ、その中でも絶対に必要な属性であるhref属性の値を検知させる。
実際にリンクを張る
Chromeの拡張機能で実装したので、これを有効にすれば下のリンクにだけ埋め込みが発生する。
- 投稿日:2021-01-17T23:51:14+09:00
Chrome拡張機能を作ってみる
前記事同様、この記事はZennに書いてましたが、難易度的にはQiita向けっぽく思えてきたため、こっちにもも同じもの投稿してちょっと反応見させてもらいます
普段使いのブラウザを自由にカスタマイズできると便利そう
バニラJS経験の多い人間として何ができるか
世にプロダクト出すとして、ササッとお出しできるものはなにかないかこのあたり考えて調べたた結果「Chrome拡張機能作ってみるのが良さそう」という結論に達したので、まず簡単なものから作ってみて、方法を掴んでいくことにしました
TL;DR
非常に簡単なChrome拡張機能を作る
自分のところにだけ読み込んで試す
これらを通じて実装方法を学ぶ
要件定義
拡張機能については、backgroundとか色々ファイルが必要になるということだが、基本的には
manifest.json
(バージョン番号とか、構造をChromeに知らせるとか)とcontent.js
(機能を記述)の2つがあれば事足りる、らしく今回の「簡単に作る」ということから、単にURLを見てなにか返す、くらいでいいか、となったため
「開いているページのURLがhttp://
=SSL非対応であればポップアップで警告を出す」
だけの簡単なものにする、と決定しました設計
どシンプルなので内部も外部もなく設計
location.protocol
でプロトコル情報(http://, https:// )が取れるので、
if(location.protcol === "http:") alert("警告で表示する内容");
コードについてはこれだけで事足りると考えましたが、後で
manifest.json
を作成する時に、動くページのURL書式を設定できる(後述、 "content_scripts" の "maches" )とわかり、こっちにhttp:のページだけで動くようにすればこのif文すらいらないとわかり、後述のようにかなりシンプルな形になりましたあとは
manifest.json
に、だいたいテンプレ通りな記述を入れれば済むのでコードを実際に書いていきます作成
結局記述としては以下のような形に
js:content.js
(function (window, document) {
alert("Here is not HTTPS!");
}) (window, document);
(即時関数にしているのは自分の手癖)manifest.json{ "name": "httpAlert", //拡張機能の名前 "version": "1.0.0", "manifest_version": 2, "description": "Notice alert in not https pages.", //拡張機能の説明 "content_scripts": [{ //動作対象ページ指定したいときとか "matches": ["http://*/"], //動作対象ページのURL書式 "js": [ "content.js" //動かしたいJSファイル ] }] }コメント無いところはほぼこのままでOKな箇所(バージョン番号なので今後変わる可能性はもちろんあり)
ここで動作対象ページをURLで指定できるので、非SSLページのみにすることで、content.js
でのif文がいらなくなりましたテスト・実装
単体テスト的なもの(コード問題ないか試す)のはChrome開発者ツールでできたため、早速実装へ
「拡張機能を管理」から「パッケージ化されていない拡張機能を読み込む」を選び、読み込み対象フォルダとして、作成した拡張機能のフォルダを選択しました
が、
ここでエラー発生「マニフェストファイルが見つからないか読み込めない」とのことですが、これについてはフォルダの構成を以下のようにしていたことが原因でした
httpAlert
└ src
├ manifest.json
└ content.js
うっかりsrc
フォルダなんか作ってその配下に各ファイルを置いたせいでマニフェストファイルが読み込めない状態になっていたようです(「httpAlert」フォルダを選んで読み込ませていた)
httpAlert
├ manifest.json
├ content.js
└ src
フォルダ構成を上記のように変更することで対応完了(srcフォルダはカラであり、残しておく意味はない)
が、この状態でもまだエラーが発生しました
今度は「マニフェストファイルが(見つかりはしたが)読み込めない」ということで
ファイル書式の間違いなのかどうか調べ、記述を見返したところ、 "matches" が以下のようになっていたことが判明
"matches": ["http://*"],
どうやら最後に/
が無いと、matchesの記述として適切でないらしく
前述の記述(完成版)の通り、最後に/
を足すことでちゃんと読み込まれ、追加されるようになりました
よかったよかった
実際に確認してみると、ちゃんとSSL非対応のページで動くのがわかります
よくベンチマークに使われる某氏のページが http:// で始まるので確認に使用
これにて実装ヨシ!
学び
・とりあえず
manifest.json
とcontent.js
があれば拡張機能は作れる
・manifest.json
はだいたい定形でもOK(用途による)、matchesのURLの最後に/
が要る
・manifest.json
はフォルダ直下に置く必要がある
・content.js
に動作を書く
あたり特に邪魔になるものでも無いのでそのまま入れ続けてます
Chromeが非SSLページに警告出すようになって既に2-3年くらい経ったと思いますが、たまにこのAlert出て、「あ、ここSSL非対応なのか」と気づくことが多く(TTFCとか)とりあえず作り方はわかったので、次は(こんなものでも)ストアに並べるか、もう少し複雑なものを作ってみるかしてみようか
- 投稿日:2021-01-17T23:29:09+09:00
JavaScriptのクラスとJSONの相互変換
SkayWayでデータを送信したい
SkyWay使ってたらStreamと別のデータを送りたいと思い、クラスをJSON,JSONをクラスに変換したくなった
が、調べても出てこなかったのでメモリファレンス曰く、
String
とbyte[]
で送信できるなので、
JSONで文字列→送信して→・・・→文字列で受信→クラスに戻す
を目指す送信するクラス
データ内容は自作のクラス
Vec.jsclass Vec{ constructor(x,y){ this.x = x; this.y = y; } } Vec.prototype.toString = function(){ return '(' + this.x + ',' + 'this.y' + ')'; }送信する関数
上記の
Vec
クラスのデータを以下のSend
関数に投げればいいようにしますなんかそこらの関数.jsfunction Send(msg){ room.send(msg); }受信する関数
受信します.jsfunction setupRoomEventHandlers(room){ room.on(`stream`,function(stream){ 接続時の処理; }); //他、peerLeaveとかcloseとかの関数 //略 //room.sendで送信したデータは「data」で受信する room.on('data',function({data,src}) => { //ここに受信時の処理 }); }クラスからJSON (class to JSON)
JSON.stringigy(クラス)
でJSONにできるので
送信したいタイミングでこんな感じにしてマウスの位置送信しますSend(JSON.stringigy(new Vec(mouseX,mouseY)));JSONからクラス(JSON to class)
受信データは
Object
が届くので、
JSON.parseでJSONにして、Object.assign(クラス,json)でクラスを復元します
なので、受信します.jsfunction setupRoomEventHandlers(room){ //略 //room.sendで送信したデータは「data」で受信する room.on('data',function({data,src}) => { let json = JSON.parse(data); console.log(Object.assign(new Vec(),json)); }); }とすることでこのように↓受信できたので、
JSON to class
&class to JSON
ができましたとさ
めでたしめでたし
- 投稿日:2021-01-17T23:21:05+09:00
【JavaScript】非同期処理と同期処理(コールバック関数・Promise)
はじめに
非同期処理と同期処理について勉強した内容をまとめます
まだまだ初心者のため認識が違っているところがあるかもしれません。ご了承ください。同期処理
一つの処理が完了するまで次の処理に進まない処理のことです。
非同期処理
同時に複数のタスクを実行できる処理のことです。
非同期処理と同期処理をレストランに例えると
※あくまでイメージです
登場人物
- シェフ(ウェイターさんから受け取った注文を調理する人)
- ウェイターさん(お客さんからのオーダーを厨房のコックさんに伝える人、また出来上がった料理をお客さんに持っていく人)
- お客さん(ウェイターさんに注文をする人)非同期レストラン
テーブルAのお客さん →ナポリタンを注文する
ウェイター →お客さんから注文を聞いて厨房のシェフに伝える
シェフ →ウェイターから聞いたナポリタンの注文の調理を始めるテーブルBのお客さん →カルボナーラを注文する
ウェイター →お客さんから注文を聞いて厨房のシェフに伝える
シェフ →ナポリタンの調理が終わり次第、カルボナーラの注文の調理を始めるシェフ →ナポリタンの調理が終わったのでウェイターに渡す。カルボナーラの調理を始める
ウェイター →ナポリタンをテーブルAのお客様に渡しにいくウェイターが複数の注文(リクエスト)を順次受け付けていくのが非同期レストランのスタイルです。
そして調理が完了(処理が完了)次第、お客さんに料理を配膳して行きます。同期レストラン
テーブルAのお客さん →ナポリタンを注文する
ウェイター →お客さんから注文を聞いて厨房のシェフに伝える
シェフ →ウェイターから聞いたナポリタンの注文の調理を始めるウェイター →ナポリタンの調理が終わるまで椅子に座って待つ
テーブルBのお客さん →カルボナーラを注文する
ウェイター →ナポリタンができるまで注文を受け付けないお客さん「」
前の処理(調理)が完了しない限り次の処理には進まないのが同期処理です。
かなり注文を取ってから提供するまでかなりの時間を要してしまいます。スレッド
スレッドとは、連続して実行される処理の一連の流れのことです。レストランの場合だと、
①ウェイターがお客さんの注文を受け取ってシェフに伝える
②シェフが料理を作る
③ウェイターが完成した料理をお客さんに配膳する
という一連の流れです。JavaScriptではメインスレッドとも呼ばれ、このスレッドで処理とレンダリング(画面をブラウザに表示させる処理)が行われます。
同期処理では、この①②③のタスクが全て完了しなければ次のオーダーに進むことができませんが、非同期処理では一時的にメインスレッドから切り離され、別のタスクを実行できるようになります。
同期処理と非同期処理の挙動を見る
まずクリックすると「クリックされたよ」とコンソールに出力されるボタンを作ります。
<button>ボタン</button>const btn = document.querySelector('button'); btn.addEventListener('click', ()=> { console.log('クリックされたよ') })同期処理
次に同期処理の関数を作ります。画面をリロードしてコンソールを見ながらひたすら先ほど作ったボタンをクリックします。
function SyncFun(ms) { //現在時刻を定数startTimeに代入 const startTime = new Date(); //現在時刻からms秒メインスレッドを占有 while(new Date() - startTime < ms); //コンソールに「fun1 done.」と出力 console.log('SyncFunが完了したよ') } //5秒間メインスレッドを占有 SyncFun(5000);挙動
・5秒間クリックしても反応がない
・5秒後「SyncFunが完了したよ」がコンソールに表示される
・次にそれまでクリックした回数の「クリックされたよ」がコンソールに表示されるこれは
while
で5秒間メインスレッドが占有されており、他の処理を割り込ませたりすることができないためこのような挙動になります。非同期処理
次に同期処理の関数の実行箇所をコメントアウトして、次の非同期の挙動を確認します。
function AsyncFun(ms) { // 非同期処理でms秒間処理をメインスレッドから切り離す setTimeout(()=> { console.log('非同期処理で5秒経ったよ') }, ms) console.log('AsyncFunが完了したよ') } //5秒間メインスレッドから処理を切り離す AsyncFun(5000);挙動
・最初に「AsyncFunが完了したよ」がコンソールに表示される
・クリックすると反応があり「クリックされたよ」が表示される
・5秒後に「非同期処理で5秒経ったよ」がコンソールに表示される
setTimeout
関数で5秒間メインスレッドから処理が切り離されるため、処理中であってもクリックイベントを実行することができます。メインスレッドは空くので、そこで他の処理を実行することができるようになります。
「非同期レストラン」のように、素早く効率的に注文を受けることができます。非同期処理の問題
同期処理も効率的に処理を実行していけるのが非同期処理のメリットですが、一方でデメリットもあります。
同期処理は上から下へ処理が進んでいきますが、非同期処理はそうはいきません。非同期処理は一旦別の処理待ちの行列(タスクキュー)に入れられて、他の処理が完了してから非同期処理が実行されます。
非同期処理は他の処理より後回しにされてしまいます。function AsyncFun() { setTimeout(()=> { console.log('非同期処理だよ') }) console.log('AsyncFunが完了したよ') } function NextFun() { console.log('次の処理だよ'); } AsyncFun(); NextFun();上記のコードでは
AsyncFun()
->NextFun()
の順番で関数を実行しています。
①「非同期処理だよ」
②「次の処理だよ」
という順番でコンソールに表示されると思いきや、実際には、
①「次の処理だよ」
②「非同期処理だよ」
の順で出力されてしまいます。
これを解決する方法として下記のようなものがあります。
①コールバック関数
②Promiese
③Async/Awaitコールバック関数
関数の引数に渡す関数のことをコールバック関数といいます。
function 関数名(コールバック関数){ //処理 //関数内でコールバック関数を実行 コールバック関数(); } function コールバック関数() { //処理 } //実行 関数名(コールバック関数);先ほどの非同期処理の例をコールバック関数にすると下記のようになります。
//引数に仮引数でcallbackという関数を入れる function AsyncFun(callback) { setTimeout(()=> { console.log('非同期処理だよ'); //関数内で引数に入れた関数を実行 callback(); }) console.log('AsyncFunが完了したよ'); } function NextFun() { console.log('次の処理だよ'); } //実行する時に引数にコールバック関数を入れる AsyncFun(NextFun);コンソールには
①「非同期処理だよ」
②「次の処理だよ」
の順で出力されます。
コールバック関数のデメリットとしては、連続して処理を行うとネストが深くなることで、コードが非常に読みづらくなってしまうことです(コールバック地獄)
//コールバック地獄の例 let val = 0; function add (callback, val) { setTimeout(function () { console.log(val ++); callback(val); }, 1000) } //読みづらい add(function(val) { add(function(val) { add(function(val) { add(function(val) { add(function(val) { },val) },val) },val) },val) },0)Promise
Promise
を用いることで非同期処理をよりコードの可読性を持ったまま書くことができます。Promiseの形式
Promise
処理の基本的な形式は下記です。new Promise(コールバック関数) .then(コールバック関数完了後の処理) .catch(エラー処理) .fainally(then/catch実行後の共通の処理)まず、
new Promise()
でインスタンスを生成します。
それに.then()
.catch()
.fainally()
とチェーンの形で処理をつなげていきます。
.then()
ではPromiseのコールバック関数実行後の処理を、.catch
では主にエラーハンドリングを行います。resolveとreject
Promiseのコールバック関数実行後、thenとcatchという処理がありましたが、引数に指定されるresolveとrejectが呼び出されることでどちらの処理に進むかが変わってきます。
resolveが呼び出されるとthenへ、rejectが呼び出されるとcatchへ処理が進みます。new Promise((resolve, reject) => { //thenに処理が進む resolve(); }).then().catch();new Promise((resolve, reject) => { //catchに処理が進む reject(); }).then().catch();もう少し正確には、
then ->resolve
が呼び出されるのを待っており、呼び出されると実行する
catch ->reject
が呼び出されるのを待っており、呼び出されると実行する
と言ったように処理を待機しています。let name = '田中太郎'; new Promise((resolve, reject) => { if (name.length >= 12) { reject(); } resolve(); }) .then(() => {console.log('Hello')}) .catch(() => { console.error('Your name is too long!')})上記は文字数が12文字を超えるとエラーとみなしてrejectを実行し、処理をcatchの方に進めています。
.then()
.catch()
の中の処理もコールバック関数にする必要があります。new Promise((resolve, reject) => { //thenに処理が進む resolve(); }) .then(() => { //コールバック関数 }).catch(() => { //コールバック関数 });thenやcatchの処理に値を渡す
例えば先ほどの処理の結果として、コンソールに「Hello、田中太郎」のように値を渡す場合は、resolveやrejectの引数に値を渡してあげます。
また、その値はthenやcatchのコールバック関数の引数として受け取ることができます。
下記の場合dataという引数に値が入ります。let name = '田中太郎'; new Promise((resolve, reject)=> { if (name.length >= 12) { //引数にnameを渡す reject(name); } //引数にnameを渡す resolve(name); }) .then(data => { // dataという仮引数で値を受け取る console.log(`Hello,${data}`); }) .catch(data => { console.log(`${data} is too long!`); })Async/Awaitは余力があれば上げます。。。
【参考】
https://youtu.be/TlB_eWDSMt4
https://www.udemy.com/share/103dh4CUATdFlaTX4=/
- 投稿日:2021-01-17T23:11:41+09:00
【備忘録】Vue:methodsとcomputedの違い
Vueの動作オプションであるmethodsとcomputedの違いに関して
new Vue({ computed: { ... }, methods: { ... } })参考
内容
下記のようなプログラムで違いを説明。
【プログラム内容】
- computedで乱数を表示
- methodsで乱数を表示
- クリックで現在日時を表示sample.html... <body> <div id="app"> <form> <input type="button" value="click" v-on:click="onclick" /> </form> <div>computed: {{randomc}}</div> <div>methods: {{randomm()}}</div> <div>daytime: {{current}}</div> </div> ... </body>sample.snew Vue({ el: '#app', data: { current: new Date().toLocaleString() }, computed: { randomc: function() { return Math.random(); } }, methods: { onclick: function(){ this.current = new Date().toLocaleString(); }, randomm: function() { return Math.random(); } }, });共通点
ページ遷移した時はcomputedとmethodsのどちらも実行される。
異なる点
1) methodsはhtml上で使うときに引数を持てる。computedは引数を持てない。
<div>methods: {{randomm(ここに引数を入れられる)}}</div>2) methodsはページが再描画されるたびに実行される。一方で、computedはページに遷移した初回のみ呼び出したときと、依存したプロパティが更新されたときのみ実行される。
つまり、今回の例だと「クリック」ボタンをクリックしたときに、現在時間が再描画されるので、その際にmethodsの乱数生成は実行されて、computedの乱数生成は実行されない。
ただし、computed: { randomc: function() { console.log(this.current); return Math.random(); } },のように現在時刻を参照すると、computedも実行される。
- 投稿日:2021-01-17T20:39:00+09:00
JavaScriptで数値を任意の文字数のゼロ埋め文字列にする
結論
数値のゼロ埋め(zfill, ゼロフィル)にはpadStart()を使いましょう。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
本題
「JavaScript zfill」とかで検索しても、
'000' + String(num)
みたいに文字列をくっつけてからslice()するみたいな手法がたくさん引っかかりますが、今のJavaScriptにはpadStart()という便利な関数がありました。const num = 123; console.log(String(num).padStart(5, '0') // '00123'使い方
冒頭のリファレンス読めばわかりますが以下のとおりです
String.prototype.padStart(<目標の文字数>, <目標への不足分を埋める文字>)
- 投稿日:2021-01-17T18:44:56+09:00
Elixir/PhoenixでTypeScriptを使えるようにする。
下記の前提で書きました。
- docker-composeを使っていてappというコンテナ名
- assets配下にnpm installしている。
install
docker-compose run app npm install typescript ts-loader source-map-loader @types/phoenix --prefix assetsakito-XPS-13-7390% docker-compose run app npm install typescript ts-loader source-map-loader @types/phoenix --prefix assets Creating zyuso_app_run ... done npm WARN source-map-loader@2.0.0 requires a peer of webpack@^5.0.0 but none is installed. You must install peer dependencies yourself. npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.3.1 (node_modules/fsevents): npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.1: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"}) npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/watchpack-chokidar2/node_modules/fsevents): npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"}) + source-map-loader@2.0.0 + ts-loader@8.0.14 + @types/phoenix@1.5.1 + typescript@4.1.3 added 22 packages from 67 contributors and audited 915 packages in 10.126s 47 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesinit
docker-compose run app ./assets/node_modules/.bin/tsc --initakito-XPS-13-7390% docker-compose run app ./assets/node_modules/.bin/tsc --init Creating zyuso_app_run ... done message TS6071: Successfully created a tsconfig.json file.change a few things from the Typescript defaults.
{ "compilerOptions": { "target": "es5", "module": "ESNext", "allowJs": true, "jsx": "react", "outDir": "./assets/dist/", "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true }, "exclude": [ "/assets/node_modules/**/*" ] }tell Webpack to recognise .ts files along with .js files
change the first module rule to:
assets/webpack.config.jsrules: [ { test: /\.(j|t)s$/, exclude: [ '/assets/node_modules/' ], use: [ { loader: 'babel-loader' }, { loader: 'ts-loader' } ] },add an outermost "resolve" key after "module" as follows:
assets/webpack.config.jsresolve: { extensions: [".ts", ".js"] },app.js
-import "../css/app.scss" +const _css = require("../css/app.scss");How to use CSS Modules with TypeScript and webpack
An example of loading ts check
Welcome to Phoenix with Typescript!
assets/js/hello.ts:
function greet(name: string): string { return "Welcome to " + name + " with Typescript!"; } export default greet;
assets/js/app.js
add the following lines toward the end:import greet from "./hello"; document.querySelector("section.phx-hero h1").innerHTML = greet("Phoenix");versions
Hex: 0.21.1
Elixir: 1.11.2
OTP: 23.2.1reference
https://levelup.gitconnected.com/elixir-phoenix-typescript-and-react-2020-edition-32ceb753705
- 投稿日:2021-01-17T18:36:12+09:00
TypeScript入門
もともとflowで型チェックをしていた業務のプロジェクトに、昨年の後半(ちょっと遅いですが、、)からTypeScriptが導入されました。
色々手こずりながら学んでますが、学んだことを備忘録的にまとめてみました。そもそもTypeScriptとは?
ざっくりいうと、TypeScriptは「型をつけられるJavaScript」です。
「静的型付け言語」という種別にあたり、変数や定数、関数の引数や戻り値などに「どの型なのか」を指定することができます。なぜTypeScriptを使うのか?その恩恵とは?
- VSコードで強力な入力補完が使えて楽
- 関数にどんなデータを渡せばよいか、どんなデータが返ってくるかが一目でわかる
- エラーチェックができる(誤った型のデータを渡したり、渡し忘れたりなど、
型の不整合
を教えてくれるのですぐ間違いに気づける)まだTS歴浅いのですが、3がTSを使う最大の価値と思いますし、実際多くの場でそう言われます。
以下、TSの公式から引用:
型は、コードの品質と読みやすさを高めることがGoogle、Microsoft、Facebookによって実証されています。 ・型があることによって、コードを書いている時点でエラーに気づくことができます。そして、すぐにエラーを修正できます。ランタイム(実行時)で、はじめてエラーに気づいて、コードに戻って修正するよりも、ずっと効率的です。開発中に早い段階でエラーに気づけるということは非常に素晴らしいことです。 ・型は、それ自体が、完璧なドキュメントです。関数のシグネチャは定理であり、関数の本体は証明です。実際に業務でTypeScriptに触れて学んだこと
ここからは実際に業務で学んだことや、他のエンジニアが書いたコードを見て気づいたことなどをバラバラですが紹介していきます。
コンポーネントのpropsへの型定義:
コンポーネントを利用する際に必要なpropsに型をGenericsで注入
以下は、ListProps型をimportして、Genericsで注入したことから、引数propsは自動でListProps型が付与されます。また、{...props}
とする事で、中に含まれる全プロパティを渡す事ができます。import { ListProps } from '~/components/List'; const Header: FC<ListProps> = props => { return ( <Wrapper> <SomeComponent {...props} /> </Wrapper> ); };constアサーション
通常のObjectや配列の中身は変更できてしまうけど、as constを使ってread only, 変更不可にでき型を安全に守る事ができる。
const FixedData = { name: 'hoge' } as const;型推論
TypeScriptは変数に型を必ず付与する必要はないのです。宣言時に代入された値から、その値の型を推論してくれるからです。例えばステートフックを使う場合、以下のように型を付与してますが、2つ目の
showMenu
に関しては型推論が効くので、はあってもなくてもOKconst [name, setName] = useState<string>(""); const [showMenu, setShowMenu] = useState<boolean>(false);三項演算子がさらに簡潔になる
propsで値を渡す時に、例えばその値がAPIから受け取るもので、何かしらの理由でその値がundefinedなどになって渡せないケースもある場合に、三項演算子などでこう書くと思います。
<SomeComponent data={fetchData ? fetchData.name : undefined} />dataを受け取るSomeComponent側で以下のように?をつけている場合、undefinedも自動で与えられます。
data? : string;なので、これはTSとは直接関係ないかもですが、こんな書き方がTS化されてから見かけるようになりました。
<SomeComponent data={fetchData?.name} />最初見たときはなんだこれ??って思いましたが、からくりが分かればなんてことないですね。
まとめ
自分の働く現場では、全てにガチガチに型を持たす事は無理なので、型に迷う時はスピード重視でとりあえずはanyにして、PRのレビューで型をどうするかアドバイスもらったりすればいいという方針です。よくTS導入したはいいけど、any型だらけになって、結局TSやめたという話は勉強会などで耳にするので、そうはならない様にはしたいですが、、
個人的にTSが導入されて当然開発のスピードが落ちましたが、初期導入にかかるこの苦労を乗り越えれば冒頭のTSの恩恵に預かれると思うので今年はTS頑張っていきます、まだまとめきれてない事が沢山あるので随時加筆、修正していきます。
参考
https://typescript-jp.gitbook.io/deep-dive/getting-started/why-typescript
https://qiita.com/markey/items/134386ee98b277f181f7
- 投稿日:2021-01-17T18:34:15+09:00
TypeScript never型
never型概要
プログラミング言語の設計には、bottom型の概念があります。それは、データフロー解析を行うと現れるものです。TypeScriptはデータフロー解析(?)を実行するので、決して起こりえないようなものを確実に表現する必要があります。
- TypeScript Deep Dive 日本語版これを読んでみても意味が分からなかったため具体例を簡単に挙げてみようと思います。
never型を使用する場面
- 常にerrorをthrowする場合
function error(message: string): never { throw new Error(message) }
- whileの中で戻り値がなく、無限ループする場合
function error(message: string): never { while(true) {} }never型が使用できない場面
- 常にerrorをthrowしない場合
function error(message: string): never { if(false) { throw new Error(message) } }
- returnの値が省略されている場合
function error(message: string): never { return }
- returnそのものが省略されている場合
function error(message: string): never { }「returnの値が省略されている場合」、「returnそのものが省略されている場合」は両方とも
undefined
型が返り値となります!!
なのでnever
型は使用出来ません。
returnを省略した場合にundefined
となることが完全に頭から抜けていました。参考文献
TypeScript Deep Dive 日本語版
TypeScriptのnever型について
JavaScript MDN
- 投稿日:2021-01-17T18:10:37+09:00
オープンストリートマップとGoogleスプレッドシートを利用した、簡単にデータを更新できるマップツール
はじめに
こちらのGoogleスプレッドシートを利用した、たぶん史上最も簡単にデータを更新できるマップツールを参考に、オープンストリートマップでも同じことをやってみました。オープンストリートマップを表示させるためにはleafletを利用しました。
leaflet
leafletとは地図データを扱うためのJavaScript ライブラリです。公式のチュートリアルやこちらの記事を参考にしました。
作成サイト
コード(元と違うところ)
全体のコードはデモサイトの方で見ていただけたらと思いますので、違う点だけ紹介します。
index.html (1)<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="crossorigin=""/> <!-- Make sure you put this AFTER Leaflet's CSS --> <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="crossorigin=""></script>leafletを使うためにleafletのCSSとleaflet.jsを呼んできます。この順番は変えてはだめだそうです。
index.html (2)var tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>', }); tileLayer.addTo(map);今回は地図としてオープンストリートマップを使うのでtileLayerにオープンストリートマップを呼んできます。leafletではほかにもいろいろな地図を表示できます。
地図上にdiv要素を追加
index.html (3)/* 作成するdivのCSS */ .sidebar { width: 250px; height: 250px; border: 1px solid #666; padding: 6px; background-color: white; font-family: Meriyo UI; font-size: 14px; overflow-y: scroll; }index.html (4)//右下にdivコントロールを表示 var sidebar = L.control({ position: "bottomright" }); sidebar.onAdd = function (map) { this.ele = L.DomUtil.create('div', "sidebar"); //divを作成 this.ele.id = "sidebardiv"; //後で使うためにidを設定 return this.ele; }; sidebar.addTo(map); var div = L.DomUtil.get('sidebardiv'); L.DomEvent.disableClickPropagation(div); //div要素上で地図のclickを制御 L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); //同じくスクロールを制御leafletではhtml要素をそのまま地図の上に表示しようとするとtileLayerの下に隠れるような形になってしまいうまく表示できませんでした。そこでこちらを参考にして地図上に独自のDOM要素を追加しました。また、そのままではサイドバー上でマウスホイールを使っても地図の方がスクロースされて使いにくかったので、こちらの一番下の回答を参考にサイドバー上での地図操作を制御しました。
(他にも細かい点は違いがありますが、省略。)できなかったこと
leafletにはGeoJSON形式のデータを一気にプロットする方法もあるのですが、そうしたときにサイドバーの名前をクリックして一つのポップアップを開く方法が分からなかったので、今回は一つずつプロットする方法にしました。
まとめ
自分は初心者なのですが、sheet2gmapを利用すると思った以上に簡単に地図に表示することができ感動しました。そこで調子に乗ってOSMでもやってみたのが今回です。他にもいろいろできそうなのでまたやってみたいです。
おまけ flyTo()
マーカーをクリックするとそのマーカーのところに飛んで行ってズームするのはflyTo()を使っています。サイドバーの名前をクリックしても飛んでいきます。動きがヌルヌルしててその動きにハマったので採用してみました。(やりすぎると酔いそうになるので注意です笑)
- 投稿日:2021-01-17T18:10:37+09:00
オープンストリートマップとGoogleスプレッドシートを利用した、簡単にデータを更新できるマップ
はじめに
こちらのGoogleスプレッドシートを利用した、たぶん史上最も簡単にデータを更新できるマップツールを参考に、オープンストリートマップでも同じことをやってみました。オープンストリートマップを表示させるためにはleafletを利用しました。
leaflet
leafletとは地図データを扱うためのJavaScript ライブラリです。公式のチュートリアルやこちらの記事を参考にしました。
作成サイト
コード(元と違うところ)
全体のコードはデモサイトの方で見ていただけたらと思いますので、違う点だけ紹介します。
index.html (1)<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="crossorigin=""/> <!-- Make sure you put this AFTER Leaflet's CSS --> <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="crossorigin=""></script>leafletを使うためにleafletのCSSとleaflet.jsを呼んできます。この順番は変えてはだめだそうです。
index.html (2)var tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>', }); tileLayer.addTo(map);今回は地図としてオープンストリートマップを使うのでtileLayerにオープンストリートマップを呼んできます。leafletではほかにもいろいろな地図を表示できます。
地図上にdiv要素を追加
index.html (3)/* 作成するdivのCSS */ .sidebar { width: 250px; height: 250px; border: 1px solid #666; padding: 6px; background-color: white; font-family: Meriyo UI; font-size: 14px; overflow-y: scroll; }index.html (4)//右下にdivコントロールを表示 var sidebar = L.control({ position: "bottomright" }); sidebar.onAdd = function (map) { this.ele = L.DomUtil.create('div', "sidebar"); //divを作成 this.ele.id = "sidebardiv"; //後で使うためにidを設定 return this.ele; }; sidebar.addTo(map); var div = L.DomUtil.get('sidebardiv'); L.DomEvent.disableClickPropagation(div); //div要素上で地図のclickを制御 L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); //同じくスクロールを制御leafletではhtml要素をそのまま地図の上に表示しようとするとtileLayerの下に隠れるような形になってしまいうまく表示できませんでした。そこでこちらを参考にして地図上に独自のDOM要素を追加しました。また、そのままではサイドバー上でマウスホイールを使っても地図の方がスクロースされて使いにくかったので、こちらの一番下の回答を参考にサイドバー上での地図操作を制御しました。
(他にも細かい点は違いがありますが、省略。)できなかったこと
leafletにはGeoJSON形式のデータを一気にプロットする方法もあるのですが、そうしたときにサイドバーの名前をクリックして一つのポップアップを開く方法が分からなかったので、今回は一つずつプロットする方法にしました。
まとめ
自分は初心者なのですが、sheet2gmapを利用すると思った以上に簡単に地図に表示することができ感動しました。そこで調子に乗ってOSMでもやってみたのが今回です。他にもいろいろできそうなのでまたやってみたいです。
おまけ flyTo()
マーカーをクリックするとそのマーカーのところに飛んで行ってズームするのはflyTo()を使っています。サイドバーの名前をクリックしても飛んでいきます。動きがヌルヌルしててその動きにハマったので採用してみました。(やりすぎると酔いそうになるので注意です笑)
- 投稿日:2021-01-17T18:05:56+09:00
色相差固定で3色選べるカラーパレットつくってみた
画面キャプチャ
できたもの
つかいかた
- マウスで
canvas
(色の円のあたり)をクリックすると、色情報が表示され、色相と彩度をスライダでも調整できるようになります。- 色相Aとマウス操作(左ボタンクリックとドラッグ)が連動します。マウスで操作した場合、もしくは色相Aをスライダで操作した場合、色相Bと色相Cの色が相対値を保って追従します。
See the Pen ColorGetter by kob58im (@kob58im) on CodePen.
※右上に表示される3色のサンプルは、文字列「■■■」の文字間隔をCSSで強引に寄せているだけの雑なつくりです。環境によっては崩れるかも。
参考サイト
- 投稿日:2021-01-17T18:05:56+09:00
色相固定で3色選べるカラーパレットつくってみた
画面キャプチャ
できたもの
つかいかた
- マウスで
canvas
(色の円のあたり)をクリックすると、色情報が表示され、色相と彩度をスライダでも調整できるようになります。- 色相Aとマウス操作(左ボタンクリックとドラッグ)が連動します。マウスで操作した場合、もしくは色相Aをスライダで操作した場合、色相Bと色相Cの色が相対値を保って追従します。
See the Pen ColorGetter by kob58im (@kob58im) on CodePen.
※右上に表示される3色のサンプルは、文字列「■■■」の文字間隔をCSSで強引に寄せているだけの雑なつくりです。環境によっては崩れるかも。
参考サイト
- 投稿日:2021-01-17T18:03:27+09:00
【JavaScript】非同期処理とasync/await ~難しいこと抜きで、まず使いはじめるための知識~ ( ´ε` )?
この記事について
この記事は、非同期処理のことをほとんど知らなかった方や、
async/awaitを使ったコードをとりあえず書いてみて覚えたいという方のために、
使用上問題ないレベルで理解してもらえるよう、カンタンに説明させていただきたいと思います。コードを読むときに「何をしたいのかわかる」
コードを書くときに「とりあえず使ってみる」
程度のレベルまで理解していただければOKという、ゆるい説明になります。
正しい知識を追求したい場合は、各自で行っていただければと思います。
(そして共有してください♡)ちなみに、async/awaitを使わない非同期処理の書き方については、あまり知見がありません。
(自分がプログラミングを学び始める時点で、すでにasync/awaitが誕生していたため。)
予めご承知おきください。非同期処理とは「上から順番に実行されるわけではない処理」のことです。
JavaScriptのコードは基本的に上から順番に実行されます。
しかしJavaScriptの中には一部、このルールを無視する処理が存在します!
この「上から順番に実行されるわけではない処理」を「非同期処理」と呼びます。非同期処理を行う関数は、主に2種類あります。
JavaScriptで非同期な処理を行う関数は、主に以下の2種類です。
- タイマー処理(
setTimeout()
やsetInterval()
など。)- HTTP通信を行う処理 …つまりAjax (
fetch()
や$ajax()
、axios()
など。)
「HTTP通信を行う」というイメージがよくわからない方への説明
JavaScriptには、
他のURLにアクセスし、そのURLが配信しているデータを取得してくるための関数があります。
これを技術的に言うと、JavaScriptでHTTP通信を行うための関数ということになります。
(JavaScript上でAPIを実行する時などに必要になってくる機能です。)
そして、このJavaScriptでHTTP通信を行う処理のことを、Ajaxと呼びます。
非同期の処理は、処理の完了を待たずに、次の処理の実行が開始されます。
(なんのためにそんなことをするかというと、この処理の実行中に、他の処理も全て止まってしまうような事態を防止するためかと思います!)(例)/* 通常の処理は、前の処理の実行が完了してから、次の処理が実行される。 */ console.log(1); console.log(2); // ↑ consoleに 1 と表示されてから実行される。 setTimeout( function(){console.log(3)}, 0 ); // ↑ consoleに 2 と表示されてから実行される。 console.log(4); // ↑ 前の処理が非同期なので、consoleに 3 と表示されているか否かに関わらず、実行される。async/awaitは、非同期処理の完了を待つための方法です。
上述の処理は非同期処理であると言いましたが、
setTimeout()
はともかく、HTTP通信を行う処理の場合、
返り値が返ってくるまえに次の処理が実行されちゃったら、意味がありません!そのため、非同期処理の完了を待ってから処理を続行するための方法が生まれました。
その方法の中で、最も新しい方法がasync/awaitなのです。つまり「非同期処理を、通常の同期処理のように実行させる仕組み」ということです。
async/awaitが現れる前から、そのためのやり方は存在していました。
非同期処理を通常の同期処理のように実行させる仕組みには歴史があり、下記のように変遷してきました。
(過去) コールバック処理 →
Promise
→async/await
(現在)ざっくり説明すると、
コールバック処理
関数の引数に関数を渡すことで、
「最初の処理がおわったら、次の処理を実行するよ」と指定する方法です。
(最近では主にタイマー関数のみで使われる方法かと思います。)setTimeout( 次の処理, 100 );
Promise
「処理が完了したかどうか」を判別することができる型のことです。
Promiseというのはクラスの名前ですので、利用する際はインスタンスを生成します。const インスタンス = new Promise( 本来実行したい関数 );Promiseインスタンスは、関数と同じように
インスタンス()
と実行すると、
本来実行したい関数
の実行が開始されます。
そして、返り値には「処理が完了したかどうか」を判別できるデータが返されます。
(本来実行したい関数
の中でreturn
されていた値は、そのデータの内部に格納されます。)このデータのおかげで、JavaScriptは非同期処理を柔軟に扱えるようになりました。
この記事では今後、このデータの事を Promise と呼びます。
async/await
Promise をそのまま扱うよりも、もっとシンプルにコードが書けるように
async/await
が登場しました。
この書き方が登場したことにより、今まで以上にシンプルに非同期処理を扱うことができるようになりました。つまり
async/await
は、
「 Promise を使う処理は書き方がめんどくさかったから、もっと簡単に書けるようにした」
というものであり、完了状態を判別するための新しい機能というわけではありません。
完了状態を判断する際には、結局 Promise (=「処理が完了したかどうか」を判別できるデータ)を利用しています。async/awaitの使い方
async
とawait
。
この2つのうち、主役となるのはawait
ですので、await
の使い方から説明していきます。今後の説明では、非同期処理を行う関数の存在が必要となるので、
例として、非同期処理を行うAPI()
という関数があるものとします。
補足
通常は、非同期処理を行う関数は、引数がもうすこし複雑な記述になります。
Ajax関数の場合
たとえば、Ajaxを実行するためのfetch
$ajax
axios
、
または 他社のAPIを実行するために作成されているオリジナルの関数の場合、
引数に 接続先の情報や、接続先に渡したい情報などを含んだオブジェクト を渡すような書き方になります。
タイマー関数の場合
また、Vue.js のthis.$nextTick
等をはじめとする タイマー関数 の場合、
引数に 時間経過後に実行したい処理(関数) を渡すような書き方になります。今回使用する
API()
は、
それらの情報を既に内部で定義してあるラッパー関数 だと想定していただければ幸いです。
(通常のコードを記述する場合も、
今回のようにラッパー関数を作成しておくか、
もしくは引数に渡す情報を前もって変数に定義するなりして、
すっきりしたコードで記述することをオススメします。)
await
の使い方
await
演算子は、非同期処理の関数を実行するときに使用します。
await
演算子を付加して実行された関数は、
処理がおわるまで次に進まないよう、待っていてくれます。(例)await API(); // ↑ await を付けない場合、処理が完了するより先に下の行の処理が実行される。 console.log('処理が完了しました。'); // ↑ await を付けたため、 API() の処理がおわってから実行される。
async
の使い方
async
装飾子は、関数を定義するときに使用します。
(async
装飾子を付けて定義された関数を、便宜上 async関数 と呼びます。)じつは
await
演算子を使用できるのは async関数 の中だけなのです。
なので、await
を利用するために仕方なく使う という認識で問題ありません。
(グローバルスコープでawait
を使えるようにする案も、検討されているとかいないとか…。)function文async function async関数(){ // await を使う処理は、この中にしか書けない。 }アロー関数const async関数 = async ()=>{ // await を使う処理は、この中にしか書けない。 }関数を async関数 として定義すると、返り値は Promise になります。
つまり、返り値で「処理が完了したかどうか」を判断できるようになります。
await
のもうひとつの役割さきほどから async関数 の返り値は Promise であると言っていますが、
じゃあ async関数 に設定していた返り値( async関数 を定義するときにreturn ooo;
と書いて指定していた値)はどうやって取り出すのか?と気になるかと思います。そこで、
await
のもうひとつの重要な役割がポイントになってきます。
await
には「 async関数 が本来返り値にする予定だった値を取り出す」という役割があります。(例)async function async関数(){ return '本来返り値にしたかったデータ'; } const awaitをつけないパターン = async関数(); // ↑ 「awaitをつけないパターン」 には Promise が入っている。 const awaitをつけるパターン = await async関数(); // ↑ 「awaitをつけるパターン」 には '本来返り値にしたかったデータ' が入っている。なので、
await
を利用することで、
処理の完了を待つこと&本来の返り値を取得することが、同時に実行できます。ちなみに、
await
は async関数 の実行時に直接つけなければいけないわけではなく、
Promise に対してであれば使うことが可能です。なので、一旦
awaitをつけないパターン
で Promise を取得しておいて、
実際に返り値を使う必要があるときにawait
で返り値を取り出してあげると、
無駄のないコードが書けます。(例)const awaitをつけないパターン = 時間がかかる関数(); // 例: 大量のデータを取得してくるAPIなど。 // 時間がかかる関数() には await をつけていないため、処理がおわったかどうかに関わらず、次の処理が進んでいく。 // この間に実行しても問題ないような、他の処理を進めておく。 // 時間がかかる関数() の返り値を使う必要があるタイミングで await をつける。 const 時間がかかる関数の返り値 = await awaitをつけないパターン; // こうすることで 「時間がかかる関数の処理が終わっていない場合だけ、処理が終わるのを待つ」 という流れにできる。複数の非同期処理が完了するのをまとめて待ちたい場合
このような処理をしたい場合のやり方が、過去記事にまとめてありますので、参考にご覧ください。
3秒で理解する async/await関数 の並列実行おわり
いかがでしょうか?
この記事をお読みいただいた方々が、
なんとなく思ったとおりにawait
を扱えるようになってくれたら幸いです。記事を書いてる途中に期間をあけすぎてしまったため、内容が中途半端かもしれませんが一旦投稿します。
説明してなかったなっていう事を思い出したら、後日追記したいと思います。ご拝読ありがとうございました。
- 投稿日:2021-01-17T17:53:34+09:00
AjaxでJavaScriptからPHPのサーバに文字列を引数にしてWebAPIを実行する例
クライアント側のhtmlファイル
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <style></style> </head> <body> <input type="text" id="input_message"> <button onclick="PostString()">WebAPI実行</button> <div id="result">ここにWebAPIの結果が入ります</div> </body> </html> <script> function PostString() { const xhr = new XMLHttpRequest(); //http://XXXXXXX.jp/test.php が今回呼び出すWebAPIを記述したphpファイル xhr.open('POST', 'http://XXXXXXX.jp/test.php'); //文字列を引数にしてWebAPIを実行する(日本語も可能です) const msg = document.getElementById("input_message").value; xhr.send(msg); xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { document.getElementById( "result" ).innerHTML = xhr.responseText; }; } } </script>XMLHttpRequestオブジェクトの
sendメソッドでデータをpostして
結果があれば
responseTextプロパティで取得しますサーバー側のphpファイル
test.php<?php echo date("Y/m/d H:i:s") . "</br>"; echo "送られた文字列は" . "</br>"; echo file_get_contents("php://input") . "</br>"; echo "です" . "</br>"; ?>file_get_contents("php://input")でpostされたメタデータが取得できるようです。
- 投稿日:2021-01-17T17:53:34+09:00
JavaScriptからPHPのサーバに文字列を引数にしてWebAPIを実行する例
クライアント側のhtmlファイル
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <style></style> </head> <body> <input type="text" id="input_message"> <button onclick="PostString()">WebAPI実行</button> <div id="result">ここにWebAPIの結果が入ります</div> </body> </html> <script> function PostString() { const xhr = new XMLHttpRequest(); //http://XXXXXXX.jp/test.php が今回呼び出すWebAPIを記述したphpファイル xhr.open('POST', 'http://XXXXXXX.jp/test.php'); //文字列を引数にしてWebAPIを実行する(日本語も可能です) const msg = document.getElementById("input_message").value; xhr.send(msg); xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { document.getElementById( "result" ).innerHTML = xhr.responseText; }; } } </script>XMLHttpRequestオブジェクトの
sendメソッドでデータをpostして
結果があれば
responseTextプロパティで取得しますサーバー側のphpファイル
test.php<?php echo date("Y/m/d H:i:s") . "</br>"; echo "送られた文字列は" . "</br>"; echo file_get_contents("php://input") . "</br>"; echo "です" . "</br>"; ?>file_get_contents("php://input")でpostされたメタデータが取得できるようです。
注意事項
htmlファイルをWEB上にホスティングせず、PCのローカルディスク上でブラウザで開いて実行しても、うまく動かないようです。
- 投稿日:2021-01-17T17:46:02+09:00
inputタグのrangeの値が文字列でドハマりした話
<input type="range" min="0" max="100" value="100" step="1" oninput="hogeValueChanged()">
こいつの
value
を使っているときに、意図しない動作に遭遇し、追っかけてみたら、文字列でした・・・(;・∀・)
仕様はちゃんと読みましょう。ってことですね。。。
input
タグのrange
のイメージ仕様記載
<input type="range"> - HTML: HyperText Markup Language | MDN
value 属性は、選択された数値を表す文字列が入った DOMString です。
- 投稿日:2021-01-17T17:46:02+09:00
inputタグのrangeの値(value)が文字列でドハマりした話
<input type="range" min="0" max="100" value="100" step="1" oninput="hogeValueChanged()">
こいつの
value
を使っているときに、意図しない動作に遭遇し、追っかけてみたら、文字列でした・・・(;・∀・)
仕様はちゃんと読みましょう。ってことですね。。。
input
タグのrange
のイメージうまくいかなかった箇所をさらしてみる
curHue1 = hue1.value;
とか代入しています。let diffHue = newHue0 - curHue0; curHue0 = newHue0; console.log("diff hue:"); console.log(diffHue); console.log("hue1 before:"); console.log(curHue1); curHue1 = (curHue1+diffHue+360)%360; console.log("hue1 after:"); console.log(curHue1); // ここで NaN になることがある curHue2 = (curHue2+diffHue+360)%360;仕様記載
<input type="range"> - HTML: HyperText Markup Language | MDN
value
属性は、選択された数値を表す文字列が入った DOMString です。と
選択された数値を表す文字列を含む DOMString。 Number として値を取得するには valueAsNumber を使用する。
- 投稿日:2021-01-17T17:17:39+09:00
【初心者】#2 React axiosでAPI データ取得
APIからJSONデータを取得して表示する
取得前回はMaterial-UIで見た目を整えましたが、
データをハードコードしただけなので、APIからデータを取得してみたいと思います。
useStateを使います。今後、Python DjangoでAPIサーバー作ってAPIサーバーを作ってみたいと思いますが、最初はJSON Placeholderというサービスで取得したいと思います。
コードは前回の引き続きになります。
【初心者】#1 Reactの基礎とMaterial-UI使って綺麗に作ってみる使用環境
- axios 0.21.1
- react 17.0.1
- material-ui 4.11.2
axiosインストールして使う
$ npm install axios画面を読み込んだ時に一度だけ、APIでデータを取得するようにします。
まず、必要なものをインポートしてmaterial-react/src/components/Content.jsimport React, {useState, useEffect} from 'react'; import axios from 'axios';
const [post, setPosts] = useState([])
オブジェクト配列で取得してくるので初期値は配列を入れていきます。JSONはアクセスしてデータを確認してみてください。
GETで取得するとこのようなデータです。
{ "userId": 1, "id": 1, "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" }のようなデータが取得できます。
useStateで一回だけ最初にAPI叩いてデータ取得
データは
useEffect(() => { axios.get('https://jsonplaceholder.typicode.com/posts') .then(res => { setPosts(res.data) }) }, [])のようにすればGETで取得して、
const [post, setPosts] = useState([])
のようにしたので、postに格納されます。※ 注意ですが、最後の
, []
がないと何度も何度もデータを取得することになるので注意!APIリクエスト回数で課金されるサービスで、これで作ってしまったら大変ですこの空の配列は、ページ開いたら一度だけ。引数をあげなければ、何か処理起こったら毎回動きます。
条件を指定して、処理を実行することもできます。結果的にこんな感じで処理を書きました。
material-react/src/components/Content.jsimport React, {useState, useEffect} from 'react'; import axios from 'axios'; import { Grid } from '@material-ui/core' import BodyCard from './BodyCard' const cardContent = { avatarUrl: "https://joeschmoe.io/api/v1/random", imageUrl: "https://picsum.photos/150" } function Content() { const [post, setPosts] = useState([]) useEffect(() => { axios.get('https://jsonplaceholder.typicode.com/posts') .then(res => { setPosts(res.data) }) }, []) const getCardContent = getObj => { const bodyCardContent = {...getObj, ...cardContent}; return ( <Grid item xs={12} sm={4} key={getObj.id}> <BodyCard {...bodyCardContent} /> </Grid> ); }; return ( <Grid container spacing={2}> {post.map(contentObj => getCardContent(contentObj))} </Grid> ) } export default Contentアバターとイメージは前回のままです。
APIで取得したデータをスプレッド演算子
const bodyCardContent = {...getObj, ...cardContent};
で結合させて、渡しているだけです。material-react/src/components/BodyCard.jsimport React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import Card from '@material-ui/core/Card'; import CardActions from '@material-ui/core/CardActions'; import CardContent from '@material-ui/core/CardContent'; import Button from '@material-ui/core/Button'; import Typography from '@material-ui/core/Typography'; import CardHeader from '@material-ui/core/CardHeader'; import Avatar from '@material-ui/core/Avatar'; import IconButton from '@material-ui/core/IconButton'; import StarBorderOutlinedIcon from '@material-ui/icons/StarBorderOutlined'; import { CardMedia } from '@material-ui/core'; const useStyles = makeStyles({ bullet: { display: 'inline-block', margin: '0 2px', transform: 'scale(0.8)', }, title: { fontSize: 14, }, pos: { marginBottom: 12, }, }); function BodyCard(props) { const { userId, id, title, body, avatarUrl, imageUrl } = props; const classes = useStyles(); const bull = <span className={classes.bullet}>•</span>; return ( <Card variant="outlined"> <CardHeader avatar={<Avatar src={avatarUrl} />} action={ <IconButton aria-label="settings"> <StarBorderOutlinedIcon /> </IconButton> } title={title} /> <CardMedia style={{ height: "150px" }} image={imageUrl} /> <CardContent> <Typography variant="body2" component="p"> {body} </Typography> </CardContent> <CardActions> <Button size="small">詳細をみる</Button> </CardActions> </Card> ); } export default BodyCardそして、渡されたデータを受け取って、タイトルと、本文を表示に使っています。
文字数によって、カードの高さずれてますね…
高さ指定、一定文字数以上は消すためにCSS追加します。makeStyleの書き換え
Material-UIでCSSを書いてみます。慣れるまでなかなか扱いにくそうですね・・・
material-react/src/components/BodyCard.jsconst useStyles = makeStyles({ . . . cHeader: { height: '50px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', "& .MuiCardHeader-content": { overflow: 'hidden' } }, cContent: { height: '200px', overflow: 'hidden' } }); function BodyCard(props) { const { userId, id, title, body, avatarUrl, imageUrl } = props; const classes = useStyles(); const bull = <span className={classes.bullet}>•</span>; return ( <Card variant="outlined"> <CardHeader avatar={<Avatar src={avatarUrl} />} action={ <IconButton aria-label="settings"> <StarBorderOutlinedIcon /> </IconButton> } className={classes.cHeader} title={title} /> <CardMedia style={{ height: "150px" }} image={imageUrl} /> <CardContent className={classes.cContent}> <Typography variant="body2" component="p"> {body} </Typography> </CardContent> <CardActions> <Button size="small">詳細をみる</Button> </CardActions> </Card> ); } export default BodyCard苦戦しながら、やったらこんな感じでうまくいきました。
"& .MuiCardHeader-content"
は生成されたクラスで、検証ツールで調べて指定してます。&
はSCSS的と同じ感じですね。知らないと、ハマりそう。あと、
textOverflow
はcssだとtext-over-flowですが、-
をなくして、キャメルケースにする必要があるのでご注意ください。次回は
次回は、react-router-domを導入してルーティングを設定します。
画像の「詳細をみる」から詳細ページを表示する機能を追加します。
- 投稿日:2021-01-17T15:57:20+09:00
【Promiseは規格?みたいなもの】
【Promiseとは】
Promiseはrejectedかfulfilledという結果が帰ってくることを約束してくれる。
そのため、それらに対するハンドリングがしやすいといったメリットがあると勉強していて感じた。実際、Node.jsの各メソッドをPromiseインスタンスに変換できるようなものが導入されたりとそのうちPromiseの規格に統一されるかもしれない。
【Promiseの挙動】
Promiseは、Promiseインスタンスを返す。
その時、Promiseインスタンスは最終的に、rejectedかfulfilledを返すが、それは以下のようにpendingな状態をreject()かresolve()で解決しないといけない。
pending ↓ / \ reject() resolve() ↓ ↓ rejected fulfilled <= これらの状態をsettledという【Promiseインスタンスをsettled状態にしてみる】
まずPromiseは
new Promise()
のようにして一度pendingな状態にする方法と、pending状態を経ずに直接fulfilled、rejected状態にする2パターンある。
// 一度pendingにする // settledにするには一度変数に入れて、setTimeoutで3000経過させるなどしないといけない function asyncFunc(){ return new Promise((resolve, reject) => { setTimeout(() => { try{ resolve('成功') }catch(err){ reject(err) }},3000) }) } console.log(asyncFunc()) => Promise { <pending> } // 直接resolve、rejected Promise.resolve('成功') => Promise { '成功' } Promise.reject(new Error('エラー')) => Promise { <rejected> Error: エラー ~~(省略)【settled状態を利用して次の処理をおこなう】
.then()、.catch()、を使えば、Promiseインスタンスがsettledになったら、実行するコールバック処理を設置できる。
以下の処理は、Promiseが1でfulfilled状態になり、その結果を.then()のnumで受け取っている。
Promiseでコールバックヘルが解決できるのはこういったことができるからだ。
Promise.resolve(1) .then(num => console.log('resolve', num))上記の.then()は省略されており、第二引数には.then()で起きたエラーの処理を受け取れるようになっている。しかし、これは.then()が多くなると長くなる。
.catch()を最後につけることで.then()で起きたエラーを全て一箇所で受け取れる。
// 同じことをしている Promise.resolve(1) .then( num => console.log('resolve', num), err => new Error('エラー') ) Promise.resolve(1) .then(num => console.log('resolve', num)) .catch(err => new Error('エラー'))【複数のPromiseを並行に処理してみる】
・ Promise.all
中のPromiseインスタンスが全部fulfilledになったら、その値でfulfilledを返す。
これだと各処理が平行に実行されるので効率がいい。
const all = Promise.all( [1,Promise.resolve('成功'), Promise.resolve('fulfilled')] ) all => Promise { [ 1, '成功', 'fulfilled' ] }・Promise.allSettled
中のPromiseインスタンスがfulfilledかrejectedかに関わらずそれぞれの結果を返す。
とりあえず結果をきにせず平行に処理したい時に便利かも。
const allSettled = Promise.allSettled([ Promise.resolve('成功'), Promise.reject(new Error('エラー')) ]) allSettled => Promise {[ { status: 'fulfilled', value: '成功' }, { status: 'rejected', reason: Error: エラー ~ (省略)Node.jsでよく使うasync awaitもPromiseインスタンスをawaitの直前に置くことで実装できるように設計されているためPromiseの理解が重要。
- 投稿日:2021-01-17T15:05:49+09:00
JavaScriptを学びたい初学者の【CODEPREP】メモ
前提
・個人忘備録です
・JavaScript初心者
・CODEPREPで完結できず調べた内容をまとめた学習内容
JavaScript入門 基本操作編を終了しました
https://codeprep.jp/books/3わからなかったところ
Strictモードって何?
単元の最初の方、Strictモードで実行させましょう、というのがあります。(↓画像みたいな)
なんぞ?と思ったのでググってみました。
ざっくり、あなたの書いたJavaScriptを厳しめにチェックするよってことだと思われる。
strictモードでは、より的確なエラーチェックが行われるため、これまでエラーにならなかったような曖昧な実装がエラー扱いになります。このことにより、コード内に存在する潜在的な問題を早期に発見しやすくなります。また、JavaScriptエンジンによる最適化処理を困難にする誤りを修正するため、strictモードのコードは非strictモードの同一コードよりも高速に実行することができる場合があるなどのメリットがあります。
参照:https://analogic.jp/use-strict/
さらに、具体的にはどんなことをチェックしてくれるか?はこちら
参照:https://www.javadrive.jp/javascript/ini/index8.html厳密等価演算子って何?
こんな(===)やつ。左辺と右辺の値が同じかな?って比較している。
じゃあ等価演算子(==)と何が違うの?//厳密等価演算子 console.log(10 === 10); //等価演算子 console.log(10 == 10);厳密等価演算子は型を見る。
等価演算子は型を見ない。詳しくはこちら
https://qiita.com/SE-studying-now/items/438cbf32a1b31b2a714b以上。
何かご指摘ありましたら、優しめな文章でコメントください笑
- 投稿日:2021-01-17T14:53:38+09:00
JavaScriptでスクロール時にふわっと表示させる方法まとめ
概要
webサイトなどを見ていると、スクロールした時に要素がふわっと現れるやつありますよね
実装方法はいろいろあって比較検討したかったので、備忘録も兼ねて以下の三つの方法をまとめますAOS
AOS(Animation On Scroll library)とは、スクロールに連動してアニメーションを簡単に実装できるJavaScriptのプラグインです。
AOSを導入すると、HTML要素にクラスを追加、数行のJavaScriptを書くだけで、スクロールアニメーションを実装することができます。また、軽量であるため、パフォーマンスの観点からも導入がしやすいです。実装方法
1 AOSの導入
パッケージのインストール
npm install --save aos@nextスクロールアニメーションを追加したいファイル内でimport
import AOS from 'aos'; import 'aos/dist/aos.css'; // .. AOS.init();もしくはHTMLファイルの<head>部分に以下を追加
※パッケージのインストールじゃないやり方<!-- css --> <link rel="stylesheet" href="/aos/dist/aos.css"> <!-- javascript --> <script src="/aos/dist/aos.js"></script> <script> AOS.init(); </script>2 スクロールアニメーションの追加
<!-- data-aos でどんなアニメーションか指定する --> <div data-aos="fade-right"> TEST </div> <!-- data-aos-○○ でアニメーションをカスタマイズ可能 --> <div data-aos="fade-up" data-aos-offset="200" data-aos-delay="50" data-aos-duration="1000" data-aos-easing="ease-in-out" data-aos-mirror="true" data-aos-once="false" data-aos-anchor-placement="top-center" > TEST2 </div>data-aos で指定できるアニメーション
タイプ 効果 Fade フェード表示。ふわっとさせる Flip 回転表示 Zoom ズーム表示。強調したいとき Slide スライド表示。スライドインしてくる感じ この他にも様々なカスタマイズができるので、ご興味ある方は以下のgithubや公式サイトをご覧ください
aosのgithub
公式サイトScrollReveal.js
ScrollRevealはスクロール時にふわっと表示させることを簡単に実装できるJavascriptのライブラリです。
基本的にやれることはAOSと似ていて軽量で使い易いライブラリです。実装方法
1 ScrollRevealの導入
パッケージのインストール
npm install scrollrevealスクロールアニメーションを追加したいファイル内でimport
import ScrollReveal from 'scrollreveal'もしくはHTMLファイルの<head>部分に以下を追加
※パッケージのインストールじゃないやり方<script src="https://unpkg.com/scrollreveal"></script>2 スクロールアニメーションの追加
<!-- アニメーションをつけたい要素に適当なクラスを付与する --> <h1 class="fuwa"> TEST </h1> <!-- JS部分 --> <script> // htmlで付与したクラス単位で、アニメーションを追加する ScrollReveal().reveal('.fuwa'); // オプションを追加し、アニメーションをカスタマイズ可能 ScrollReveal().reveal('.fuwa', { duration: 1000, // アニメーションの完了にかかる時間 reset: true // 何回もアニメーション表示するか }); </script>オプションに関しては、他にもいろんなものがあるので、ご興味ある方は以下の公式サイトをご覧ください
公式サイト生のJavascript
生のJavascriptでやる場合はこんな感じに書けます
html<div> <p class="fuwa">TEST</p> </div>css.fuwa { opacity: 0; visibility: hidden; transform: translateX(30px); transition: all 1s; } .fuwa.show { opacity: 1; visibility: visible; transform: translateX(0px); }jsfunction showElementAnimation() { const elements = document.getElementsByClassName('fuwa'); const showTiming = window.innerHeight > 768 ? 200 : 40; const scrollY = window.pageYOffset; const windowH = window.innerHeight; for (let i=0;i<elements.length;i++) { // 要素の寸法と、そのビューポートに対する位置を取得 const clientRect = elements[i].getBoundingClientRect(); const elemY = scrollY + clientRect.top; if(scrollY + windowH - showTiming > elemY) { elements[i].classList.add('show'); } else if(scrollY + windowH < elemY) { // スクロールを上に戻して再度非表示にする場合はこちらを記述 elements[i].classList.remove('show'); } } } showElementAnimation(); window.addEventListener('scroll', showElementAnimation);まとめ
個人的には上記の三つの中では、ScrollReveal.jsが一番良いかなと感じました。
理由としては、クラス単位でカスタマイズが可能なので同じコードを何回も書かなくて済むのと
(要素ごとにいろんなパターンのアニメーションを書きたい場合はめんどくさいかも、、)
本記事執筆時でAOSよりScrollReveal.jsの方がgithubのコミット履歴が新しいからです。ほかにもいろんなライブラリがあると思うので、もっと使いやすいやつを見つけたら追記していこうと思います。
- 投稿日:2021-01-17T14:37:15+09:00
【Vue.js】Vue.js最初の一歩
はじめに
最近Vue.jsを勉強しています。
自分の備忘を兼ねて、初歩的な内容を書いていきます。基本
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Sample</title> <meta name="description" content="sample"> <!-- Vue.jsをインストール --> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> </head> <body> <!-- Vue.jsを適用する場所 --> <div id="app"> </div> <script> //最初にVueクラスをインスタンス化 new Vue({ //elオプション:Vueを適用する場所を指定 el: '#app', //dataオプション:Vueで使用するデータを用意する data: { message: 'Hello Vue!' } }); </script> </body> </html>まず
Vue
クラスをインスタンス化します。
そして、el
オプションにより、Vueを適用する場所を指定します。今回はapp
というidを持つdiv
要素に対して適用するので、el: '#app'
としています。
最後に、data
オプションで、使用するデータを用意します。「プロパティ:値」のように記述するので、今回は'Hello Vue!'
という文字列をmessage
プロパティにセットしたことになります。
このデータを表示させてみます。
HTML内で{{プロパティ名}}
のように記述すると、該当するプロパティの値が呼び出され、表示されます。<body> <div id="app"> <!-- messageデータを呼び出し --> {{message}} </div> <!-- Vue適用範囲外なので呼び出されない --> {{message}} <script> new Vue({ el: '#app', data: { message: 'Hello Vue!' } }); </script> </body>結果
message
プロパティの値が呼び出され、表示されました。
2段目はVueが適用されるdiv
要素の外なので、データが呼び出されず、そのまま表示されています。v-forディレクティブ
Vueでは、ディレクティブというものを使って、表示に関する様々な制御を行えます。
v-forディレクティブは、for文のような繰り返し処理を行います。<body> <div id="app"> <ul> <!-- 配列itemsから要素itemを繰り返し取り出す --> <li v-for="item in items"> <!-- 取り出したitemのname属性を表示 --> {{item.name}} </li> </ul> </div> <script> new Vue({ el: '#app', data: { //表示する要素を格納した配列 items: [ {name: 'item1'}, {name: 'item2'}, {name: 'item3'} ] } }); </script> </body>まず
data
オプションで、複数の要素を格納した配列を用意しておきます。
次に繰り返し表示したいHTML要素のタグにv-for="item in items"
といった記述を追加します。
そしてその要素の中に{{item.name}}
と記述します。
これで、配列の要素を繰り返し取り出し、それらを次々に表示させています。
状況によってリストの内容が変化する場合等に便利そうですね。v-onディレクティブ
v-onディレクティブはイベント制御を行うことができます。
<body> <div id="app"> <!-- クリックしたらcountを1増加させる --> <button v-on:click="count += 1">ボタン</button> {{count}} </div> <script> new Vue({ el: '#app', data: { //ボタンが押されるたびに1ずつ増える count: 0 } }); </script> </body>ボタンに
v-on:click="count += 1"
と記述することにより、クリックすると変数count
を1増加させるという処理を付加できます。
変数count
は画面に表示されているので。増加していく様子を見ることができます。v-ifディレクティブ
v-ifディレクティブを使うと、条件によって表示させる内容を変化させることができます。
<body> <div id="app"> <button v-on:click="count += 1">ボタン</button> {{count}} <!-- 条件によって表示内容を変化させる --> <div v-if="count % 2 === 0">偶数</div> <div v-else>奇数</div> </div> <script> new Vue({ el: '#app', data: { //ボタンが押されるたびに1ずつ増える count: 0 } }); </script> </body>
div
タグにv-if="count % 2 === 0"
と記述することで、この条件を満たす場合に要素が表示されるようにしました。
これにより、count
が偶数か奇数かを判定して表示するようにしています。ボタンを押すたびに表示が切り替わります。v-showディレクティブ
v-ifディレクティブと似ています。記載内容がtrueなら要素が表示されます。
<body> <div id="app"> <button v-on:click="show = !show">ボタン</button> {{show}} <!-- 値がtrueなら表示される --> <div v-show="show">表示</div> <!-- 値がtrueでなければ表示される --> <div v-show="!show">非表示</div> </div> <script> new Vue({ el: '#app', data: { show: true } }); </script> </body>ボタンを押すと、
show
について、true
・false
の切り替えが行われます。
これにより、表示される要素が切り替わります。おわりに
Vue.jsは、HTMLを自在に操作できて便利そうですが、なかなか奥が深いようです。頑張って勉強を続けます。
(参考)
この本で勉強しています。
Webデザインの現場で使えるVue-jsの教科書
- 投稿日:2021-01-17T14:35:20+09:00
すごく無理やり「同じ要素が連続する」配列の判定
「同じ要素が連続である」配列の判定を行いたくていろいろ試行錯誤したので、ここにメモ。
sample.jslet array = [12, 2, 0, 0, 0, 5];↑の配列のような場合だと、「0が3回連続である」配列だから
true
を返す、というような処理を書きたい試したこと①:filterメソッドでできるかいったん確認
sample.jslet target_array = [0, 0, 0]; let array = [12, 2, 0, 0, 0, 5]; const result = array.filter(item => item === target_array); console.log(result); // 実行結果 // []「0が3回連続で続く配列」と同じ要素を取り出そうとして
filter
メソッドで試したが、配列の要素に配列を当てはめようとしているのでもちろんできない。試したこと②:配列の重複を利用
2つの配列を比較して重複を取り出すことにしようってなった。
(参考記事)
https://www.dkrk-blog.net/javascript/duplicate_an_arraysample.jslet target_array = [0, 0, 0]; let array = [12, 2, 0, 0, 0, 5]; const result = array.filter(item => target_array.includes(item)).length === 3 console.log(result); // 実行結果 // true
target_array
の中のarray
と一致する要素を取得、その個数が3つあるならtrue
を返す。「これで
true
になった!!」って思っていたけど、いろいろ配列を変えてみると思い通りの判定にならず。。。。これは「
target_array
の要素の中でarray
の要素と一致するものは3個かどうか」っていう処理だから、目的の処理ではないって現実を突きつけられる。。。そもそも、配列の重複で確認できるは「配列の要素」だけで、「配列の順番」は同時に確認できないだろって自分でツッコミしてました。
ポイントにした点
配列の重複を確認しているときに、「配列に同じ要素が並んでいる」という考え方ではなく「配列の要素が連続して同じ」っていう視点に切り替えることにした。
端的に言えば、「配列の同じ要素のインデックス番号に取得してそれが連続しているかどうか」っていう処理を書こうってなった。
強引にインデックス番号を取得:findIndex
対象の要素のインデックス番号を取得するのは、
findIndex
メソッドだからそれを使用することにした。ここで問題になってくるのが、
findIndex
メソッドは最初のインデックス番号しか取得できないってことだが、これについては強引に突破することにした。sample.jslet array = [12, 2, 0, 0, 0, 5]; let first_zero = parent.findIndex(element => element === 0); if (first_zero !== -1) { delete array[first_zero]; } console.log(array); let second_zero = parent.findIndex(element => element === 0); if (second_zero !== -1) { delete array[second_zero]; } console.log(array); let third_zero = parent.findIndex(element => element === 0); if (first_zero !== -1 && second_zero !== -1) { array[first_zero] = 0; array[second_zero] = 0; } console.log(array); console.log(first_zero); console.log(second_zero); console.log(third_zero); let first_condition = first_zero + 1 === second_zero let second_condition = second_zero + 1 === third_zero if (first_condition && second_condition) { console.log('成功'); } else { console.log('失敗'); } // 実行結果 // [12, 2, , 0, 0, 5] // [12, 2, 0, , 0, 5] // [12, 2, 0, 0, 0, 5] // 2 // 3 // 4 // 成功「最初のインデックス番号しか取れないなら、取得する度に要素を空にしよう」という考えで
delete
演算子を使用。空のまま放置はさすがにまずいから、
third_zero
を取得したタイミングで0
を戻す処理も一緒に添える。ここで取得してきたインデックス番号が連番していたら、「成功」が呼び出される仕組み。
まとめ
これで一応目的の処理が問題なくできたけど、綺麗なコードではないから別の通りを考えないといけないなと思いつつある程度に煮詰まったからここにアウトプットしました。
もっと最適な処理がある場合は、コメントで教えていただけると幸いです。
最後まで読んでくれてありがとうございました。
- 投稿日:2021-01-17T13:03:46+09:00
jsの関数巻き上げについて
var myname = "global"; function func() { //1 console.log(myname); //undifined ///2 var myname = "local"; console.log(myname); //local } func();jsによって
var myname
だけが関数の先頭である1と2の間で実行される宣言を
let
にすると
```
let myname = "global";function func() {
console.log(myname); //ReferenceError: Cannot access 'myname' before initialization
let myname = "local";
console.log(myname); //エラーで実行されない
}func();
```
このようになり関数の巻き上げを防ぐことが出来る参考
https://developer.mozilla.org/ja/docs/Glossary/Hoisting
https://analogic.jp/hoisting/
- 投稿日:2021-01-17T12:55:35+09:00
サーバーにある画像をFileに変換して再度アップロード(コピー)する方法
始めに
サーバーに画像をアップロードするためにinputタグを使って以下のような感じでアップロードしていました。
ファイルアップロードconst elInput = document.getElementById('file'); const file = elInput.files[0]; // FileインスタンスをFormDataに含めて送信 const formData = new FormData(); formData.append('file', file); axios.request({ method: 'post', url: 'api/upload', data: formData, });基本的にはこれで問題ないのですが、画像を含むその他の情報もまとめてコピーする機能が欲しいと言われた際に、そのままでは上手くできないなと思いました。
サーバーから取得した画像を上手くFileに変換できれば同じようにFormDataを使って送信すれば良いのですが、それを調べるのに少し時間がかかりました(そもそもできるのかも分かりませんでしたし)。
結論としてはできましたので、そのやり方を備忘録として残したいと思います。サーバーにある画像をFileに変換する方法
まず始めに画像をバイナリとして取得する必要があります。その次にBlobに変換し、最後Fileに変換することができます。
Fileに変換できればあとは最初に書いたアップロード方法でアップすれば終了です。画像をバイナリで取得し、Fileに変換する方法// バイナリで取得する const response = await axios.request({ method: 'get', responseType: 'arraybuffer', url: 'image url', }); // レスポンスヘッダーからimage/pngなどのファイル形式を受け取れるので取得する const fileType = response.headers['content-type']; // バイナリ -> Blob -> Fileと変換 const blob = new Blob([response.data], { type: fileType }); const file = new File([blob], 'copy filename', { type: fileType });終わりに
以上がサーバーにある画像をFileに変換して再アップロード(コピー)する方法でした。APIから取得したデータもFileに変換できるというのはちょっと驚きでした。バイナリの扱いは結構難しいですが、しっかり学べば結構なんでもできそうだなぁと感じました。
参考記事
- 投稿日:2021-01-17T11:55:42+09:00
javascript で学ぶオブジェクト指向
はじめに
プログラミングの勉強を進めていくとオブジェクト指向の話が出てきます。
なんとなく分かったつもりでいても、オブジェクト指向言語を使えばいいというものではなく、その考え方をちゃんと理解しながら使っていかなければ身につきません。ここでは例として生活の中で分かりやすい対象としてテレビと新聞を見る動作をjavascriptで表現してみました。またそのソースコードに少しずつオブジェクト指向を取り込んで発展していくように書いています。
皆さんもなにか身の回りのものをスクリプト化して挑戦してみてください。オブジェクト指向とは
ソフトウェアの設計や開発において、操作手順よりも操作対象に重点を置く考え方をオブジェクト指向といいます。すでに存在するオブジェクト(データと手続き)については、利用に際してその内部構造や動作原理の詳細を知る必要はなく、外部から指示を送れば機能するため、特に大規模なソフトウェア開発において有効な考え方であるとされています。
例えば、テレビを操作する際には、テレビ内部でどのような回路が働いているかを理解する必要はありません。ただテレビの操作方法だけを知っていれば、それでテレビを使うことができます。すなわち、テレビという「オブジェクト」は電源をいれれば電波を拾って映像が表示されるものだということを知っており、それを利用するためには(例えばリモコンで)適切な指示を与えるだけでよいわけです。
ネームスペース (名前空間)
手続き型の問題
プログラムを構築する際に手続きに沿って処理を書いた方が初心者のうちは理解しやすく書きやすいでしょう。しかし、例えばテレビを閲覧する処理をプログラムで書き出した場合、ザッピングで次々にチャンネルを変える処理を書こうとすると同じ処理を何度も書くことになります。
コピペでいくつも複製していけばいいと思う人もいるかもしれませんが、行数が増えるとだんだんソースコードが読みにくくなり、間違いも起きやすくなります。
手続き型(ベタ打ち)の例
// デフォルト設定 const newspaper_name = "毎朝新聞"; let newspaper_page = "表紙"; const television_name = "リビングのテレビ"; let television_power = "off"; let television_channel = 1; // 実行 newspaper_page = "テレビ欄"; console.log( newspaper_name + "の" + newspaper_page + "ページを開きました。" ); // 毎朝新聞のテレビ欄ページを開きました。 console.log( newspaper_name + "の" + newspaper_page + "ページを読みます。" ); // 毎朝新聞のテレビ欄ページを読みます。 if( television_power != "on" ) { television_power = "on"; console.log( television_name + "を点けました。" ); // リビングのテレビを点けました。 } television_channel = 10; console.log( television_name + "のチャンネルを" + television_channel + "に変えました。" ); // リビングのテレビのチャンネルを10に変えました。 console.log( television_name + "で" + television_channel + "チャンネルを観ます。" ); // リビングのテレビで10チャンネルを観ます。 television_channel = 4; console.log( television_name + "のチャンネルを" + television_channel + "に変えました。" ); // リビングのテレビのチャンネルを4に変えました。 console.log( television_name + "で" + television_channel + "チャンネルを観ます。" ); // リビングのテレビで4チャンネルを観ます。 television_channel = 1; console.log( television_name + "のチャンネルを" + television_channel + "に変えました。" ); // リビングのテレビのチャンネルを1に変えました。 console.log( television_name + "で" + television_channel + "チャンネルを観ます。" ); // リビングのテレビで1チャンネルを観ます。そこで、処理を関数化すると実行部分が1行につき1処理になって処理がわかりやすくなります。
手続き型(処理を関数化したもの)
// デフォルト設定 const newspaper_name = "毎朝新聞"; let newspaper_page = "表紙"; const television_name = "リビングのテレビ"; let television_power = "off"; let television_channel = 1; // 関数 function changeNewspaperPage(new_page) { newspaper_page = new_page; console.log( newspaper_name + "の" + newspaper_page + "ページを開きました。" ); } function readNewspaperPage() { console.log( newspaper_name + "の" + newspaper_page + "ページを読みます。" ); } function onTelevisionPower() { if( television_power == "on" ) return; television_power = "on"; console.log( television_name + "を点けました。" ); } function changeTelevisionChannel(new_channel) { television_channel = new_channel; console.log( television_name + "のチャンネルを" + television_channel + "に変えました。" ); } function watchTelevisionChannel() { console.log( television_name + "で" + television_channel + "チャンネルを観ます。" ); } // 実行 changeNewspaperPage( "テレビ欄" ); // 毎朝新聞のテレビ欄ページを開きました。 readNewspaperPage(); // 毎朝新聞のテレビ欄ページを読みます。 onTelevisionPower(); // リビングのテレビを点けました。 changeTelevisionChannel( 10 ); // リビングのテレビのチャンネルを10に変えました。 watchTelevisionChannel(); // リビングのテレビで10チャンネルを観ます。 changeTelevisionChannel( 4 ); // リビングのテレビのチャンネルを4に変えました。 watchTelevisionChannel(); // リビングのテレビで4チャンネルを観ます。 changeTelevisionChannel( 1 ); // リビングのテレビのチャンネルを1に変えました。 watchTelevisionChannel(); // リビングのテレビで1チャンネルを観ます。「実行」以下がスッキリしました。
しかし、今度は関数が多くなってしまってその処理を探すのに苦労します。変数名や関数名が重複しないように気を配る必要があるので変数名や関数名は長くなりがちになり、関数内の処理の1行1行が横に長くなってしまいます。
名前空間
そこで、この長い変数名や関数名をオブジェクト(もの)別にグルーピングします。オブジェクト内の変数や関数は
this.
で表せられるため、メソッド内の処理がシンプルになります。
このように変数名や関数名の名前の衝突を防ぐためにグループ化することを名前空間を使うといいます。プロパティとメソッド
変数はオブジェクト内では属性(プロパティ)、関数はオブジェクト内ではメソッドといいます。
名前空間を使用した例
// 新聞 const Newspaper = { // プロパティ name : "毎朝新聞", page : "表紙", // メソッド changePage : function(new_page) { this.page = new_page; console.log( this.name + "の" + this.page + "ページを開きました。" ); }, readPage : function() { console.log( this.name + "の" + this.page + "ページを読みます。" ); } } // テレビ const Television = { // プロパティ name : "リビングのテレビ", power : "off", channel : 1, // メソッド onPower : function() { if( this.power == "on" ) return; this.power = "on"; console.log( this.name + "を点けました。" ); }, changeChannel : function(new_channel) { this.channel = new_channel; console.log( this.name + "のチャンネルを" + this.channel + "に変えました。" ); }, watchChannel : function() { console.log( this.name + "の" + this.channel + "チャンネルを観ます。" ); } } // 実行 Newspaper.changePage( "テレビ欄" ); // 毎朝新聞のテレビ欄ページを開きました。 Newspaper.readPage(); // 毎朝新聞のテレビ欄ページを読みます。 Television.onPower(); // リビングのテレビを点けました。 Television.changeChannel( 10 ); // リビングのテレビのチャンネルを10に変えました。 Television.watchChannel(); // リビングのテレビの10チャンネルを観ます。 Television.changeChannel( 4 ); // リビングのテレビのチャンネルを4に変えました。 Television.watchChannel(); // リビングのテレビの4チャンネルを観ます。 Television.changeChannel( 1 ); // リビングのテレビのチャンネルを1に変えました。 Television.watchChannel(); // リビングのテレビの1チャンネルを観ます。名前空間のメリット
名前空間を使用しないと以下の問題が起きます。
- 全てがグローバル変数になってしまう。グローバルスコープが汚染される。
- 変数や関数が衝突しやすい。
- 衝突を避けるために変数や関数名が長くなりがち。
名前空間を使うとスッキリ整理でき、以下の利点が生まれます。
- グローバルスコープが汚染されにくくなる。
- 名前空間内では衝突を気にしなくていい。
- ものや意味などでまとめられ、作りやすくなる。
- ソースコードが読みやすくなる。
- 開発を分担しやすくなる。
カプセル化
カプセル化とは
テレビの電源を入れたりチャンネルを変えたりするとき、普通はリモコンで操作します。その時、テレビの中で何が起きているか、どういう構造で動いているかは知らなくても、テレビを操れます。たとえテレビの構造を知っているエンジニアだったとしてもリモコンを使います。わざわざテレビを分解して電源を入れる必要はなく、そんなことをしたら逆に誤作動を起こしたり、テレビが壊れたりするでしょう。
プログラムも同じように、中の複雑な処理は隠蔽し、インターフェースだけを提供することでシンプルで分かりやすく便利になります。これをカプセル化といいます。
オブジェクト指向はこれをオブジェクト単位で行います。
カプセル化されていないと
カプセル化をしないと以下の問題が起きます。
- プロパティやメソッドを外部から好きなように変更できてしまう。想定外の変更を許してしまう。
- バグが見つかった場合に、想定外の処理をしている箇所がオブジェクトの外側にある可能性があるため、改修に時間がかかってしまう。
- オブジェクトの中身を考慮してプログラムを組む必要があり、開発に時間がかかる。
下記は名前空間でまとめただけのものですが、プロパティもメソッドもグローバルスコープで全てアクセスできてしまうため間違った使い方をしてしまう可能性があります。
※Television は const 宣言していますが、そのプロパティは可変になります。
// テレビ const Television = { // プロパティ name : "リビングのテレビ", power : "off", channel : 1, // メソッド onPower : function() { if( this.power == "on" ) return; this.power = "on"; console.log( this.name + "を点けました。" ); }, changeChannel : function(new_channel) { this.channel = new_channel; console.log( this.name + "のチャンネルを" + this.channel + "に変えました。" ); }, watchChannel : function() { console.log( this.name + "の" + this.channel + "チャンネルを観ます。" ); } } // 実行 Television.power = "on"; // 簡単に書き換えられてしまう。出力はされない。 Television.channel = -10; // -10という想定していないチャンネルを設定できてしまう。 Television.watchChannel(); // 出力: リビングのテレビの-10チャンネルを観ます。スコープ
変数が利用できる範囲のことをスコープといいます。
javascriptは関数内で宣言された変数は関数の内側でしか使えません。関数の中にさらに関数がある場合、内側の関数は外側の関数内にある変数は利用できますが、外側からは内側で宣言される変数は利用できません。
クロージャ
通常は関数の中に書かれたプロパティは引数で値を渡して中で書き換えでもしない限り変更されません。引数で変更したとしても関数内のプロパティは関数の処理が終了したら開放され、初期化されてしまいます。
しかし、関数を変数として格納してしまえば、その関数が使えるスコープ内の変数も一緒に保持するので、オブジェクトの状態(プロパティ)を保持することができます。この関数とその関数の環境そのものをセットで記憶し、アクセスできる関数のことをクロージャといいます。
このjavascriptのスコープとクロージャの性質を利用してプライベートなプロパティ/メソッドを定義することができます。
以下はクロージャを使ってカプセル化した例です。
クロージャを使ってカプセル化した例
// テレビ function Television() { // プロパティ const name = "リビングのテレビ"; let power = "off"; let channel = 1; // メソッド this.onPower = function() { if( power == "on" ) return; power = "on"; console.log( name + "を点けました。" ); } this.changeChannel = function(new_channel) { // 新しいチャンネルを代入する前に値が正しいかチェック new_channel = parseInt(new_channel); if ( isNaN(new_channel) ) return; // チャンネルは数字意外は無視。 if( new_channel < 1 || new_channel > 100 ) return; // チャンネルは最低1、最高100。それ以外は無視。 channel = new_channel; console.log( name + "のチャンネルを" + channel + "に変えました。" ); } this.watchChannel = function() { console.log( name + "の" + channel + "チャンネルを観ます。" ); } } // インスタンス生成 const LivingTelevision = new Television(); // 実行 LivingTelevision.onPower(); // 出力: リビングのテレビを点けました。 LivingTelevision.channel = -10; // プライベートなプロパティとは別のプロパティにアクセスしている。 LivingTelevision.changeChannel( -10 ); // 誤ったチャンネル指定は無視される。 LivingTelevision.watchChannel(); // 出力: リビングのテレビの1チャンネルを観ます。 console.log(LivingTelevision.channel); // -10 と出力される。オブジェクト形式ではなく関数形式にすることで、プロパティを const 宣言できます。関数内部で変数を宣言しているので、関数の外からは変数を操作できません。プロパティをメソッド経由でしか設定できないようにすれば、値を代入する前に毎回チェックすることができ、想定していない値を設定されることを防げます。
以下は即時関数を使ってカプセル化した例です。即時関数の場合はnewしなくても実行できます。上記の例との違いはオブジェクトをreturnで返しています。Television には即時関数全体ではなくこのオブジェクトが代入されますが、javascriptは内側からは外側の変数にアクセスできるため、このオブジェクトから即時関数内の変数にもアクセス可能です。
ただ、この場合は逆にコンストラクタがないためnewでインスタンスを生成できません。複数のインスタンスを生成したい場合は上記の例を使ってください。
即時関数を使ってカプセル化した例
// テレビ const Television = (function () { // プロパティ const name = "リビングのテレビ"; let power = "off"; let channel = 1; // メソッド return { onPower : function() { if( power == "on" ) return; power = "on"; console.log( name + "を点けました。" ); }, changeChannel : function(new_channel) { // 新しいチャンネルを代入する前に値が正しいかチェック new_channel = parseInt(new_channel); if ( isNaN(new_channel) ) return; // チャンネルは数字意外は無視。 if( new_channel < 1 || new_channel > 100 ) return; // チャンネルは最低1、最高100。それ以外は無視。 channel = new_channel; console.log( name + "のチャンネルを" + channel + "に変えました。" ); }, watchChannel : function() { console.log( name + "の" + channel + "チャンネルを観ます。" ); } } }()); // 実行 Television.onPower(); // 出力: リビングのテレビを点けました。 Television.changeChannel( -10 ); // 誤ったチャンネル指定は無視される。 Television.watchChannel(); // 出力: リビングのテレビの1チャンネルを観ます。カプセル化のメリット
カプセル化をすると以下の利点が生まれます。
- オブジェクトのデータの処理はオブジェクトに集約され、バグの発生を防ぐ。
- バグが見つかってもオブジェクトの内側だけ確認すればよくなる場合が多い。
- 使う側はシンプルでわかりやすくなる。
- 使う側はデータの処理が減るので組み立てがシンプルになる。
- 開発を分担しやすくなる。
抽象化
新聞は新聞社によって特徴があり同じネタでも違いがありますが、「新聞」ということでは共通しており朝刊、夕刊といった共通の属性(プロパティ)があります。
テレビもブラウン管テレビ、液晶テレビ、4Kテレビなど形や大きさ、構造に違いがありますが、「テレビ」ということでは共通しており、電源を入れる、チャンネルを変えるという共通の機能や行動(メソッド)があります。
また、テレビと新聞は情報を得るというメソッドは同じであり、情報を得る「媒体(メディア)」というグルーピングができます。このように、具体的な対象から具体性を排除し、共通の特徴によってグルーピングし、共通の性質を抽出することで、より汎用的な概念に構成していくことを抽象化といいます。
この抽象化を経ることで継承やポリモーフィズム(多様性)が成り立つので抽象化はとても重要です。
継承
抽象化されたオブジェクトのプロパティやメソッドを引き継いで、より具体化された別のオブジェクトとして定義することを継承といいます。
以下はその抽象化、継承を取り入れた例です。
// メディア function Media(media_name) { // プロパティ let name = media_name; // メソッド this.getName = function() { return name; } this.getInformation = function() // 各媒体ごとに異なる動作をするメソッドになる { console.log( name + "で情報を得る。" ); } } // テレビ function Television(media_name) { Media.call(this, media_name); // 親クラスのコンストラクタを call で呼び出して継承する。 // テレビとしての追加プロパティ let power = "off"; let channel = 1; // メソッド this.onPower = function() { if( power == "on" ) return; power = "on"; console.log( this.getName() + "を点けました。" ); } this.changeChannel = function(new_channel) { // 新しいチャンネルを代入する前に値が正しいかチェック new_channel = parseInt(new_channel); if ( isNaN(new_channel) ) return; // チャンネルは数字意外は無視。 if( new_channel < 1 || new_channel > 100 ) return; // チャンネルは最低1、最高100。それ以外は無視。 channel = new_channel; console.log( this.getName() + "のチャンネルを" + channel + "に変えました。" ); } this.watchChannel = function() { console.log( this.getName() + "の" + channel + "チャンネルを観ます。" ); } this.getInformation = function() // 親と同じメソッド名を定義することでオーバーライドする { this.onPower(); // 情報を得るためにはまずは電源を入れる this.watchChannel(); } } // 新聞 const Newspaper = function(media_name) { Media.call(this, media_name); // 親クラスのコンストラクタの呼び出しには call を使用 // 新聞としての追加プロパティ let page = "表紙"; // メソッド this.changePage = function(new_page) { page = new_page; console.log( this.getName() + "の" + page + "ページを開きました。" ); } this.readPage = function() { console.log( this.getName() + "の" + page + "ページを読みます。" ); } } // インスタンス生成 const MaiasaNewspaper = new Newspaper("毎朝新聞"); const LivingTelevision = new Television("リビングのテレビ"); // 実行 MaiasaNewspaper.getInformation(); // 毎朝新聞で情報を得る。 LivingTelevision.getInformation(); // リビングのテレビを点けました。リビングのテレビの1チャンネルを観ます。
ポリモーフィズム(多様性)
テレビで情報を得る場合は電源を入れて目的のチャンネルに合わせ、映像と音で情報を得ます。新聞で情報を得るには目的のページを開いて、活字情報から情報を得ます。異なるオブジェクトですが似たような処理があることが分かります。
この共通する「情報を得る」という処理を予め決めたメソッド名で作るようにしておき、そのメソッドの中身は各オブジェクトに合わせて最適化しておくことで、メインの処理を作る側はいちいちオブジェクトによって処理を分岐する必要がなくなります。このように、同じ呼び出し方なのに異なる動作(多様な動作)をするという特性をポリモーフィズムといいます。
抽象化ですでに getInformation という統一されたメソッドを用意できていますが、ページやチャンネルを変えるという動作を同じメソッド changeView で実現してみます。ついでにオブジェクトを連想配列に格納してループさせてみます。
// メディア function Media(media_name) { // プロパティ let name = media_name; // メソッド this.getName = function() { return name; } this.getInformation = function() // 各媒体ごとに異なる動作をするメソッドになる { console.log( name + "で情報を得る。" ); } this.changeView = function(view) { // 各オブジェクト側で実装してね } } // テレビ function Television(media_name) { Media.call(this, media_name); // 親クラスのコンストラクタを call で呼び出して継承する。 // テレビとしての追加プロパティ let power = "off"; let channel = 1; // メソッド this.onPower = function() { if( power == "on" ) return; power = "on"; console.log( this.getName() + "を点けました。" ); } this.changeChannel = function(new_channel) { // 新しいチャンネルを代入する前に値が正しいかチェック new_channel = parseInt(new_channel); if ( isNaN(new_channel) ) return; // チャンネルは数字意外は無視。 if( new_channel < 1 || new_channel > 100 ) return; // チャンネルは最低1、最高100。それ以外は無視。 channel = new_channel; console.log( this.getName() + "のチャンネルを" + channel + "に変えました。" ); } this.watchChannel = function() { console.log( this.getName() + "の" + channel + "チャンネルを観ます。" ); } this.getInformation = function() // 親と同じメソッド名を定義することでオーバーライドする { this.onPower(); // 情報を得るためにはまずは電源を入れる this.watchChannel(); } this.changeView = function(view) { this.changeChannel(view); this.watchChannel(); } } // 新聞 const Newspaper = function(media_name) { Media.call(this, media_name); // 親クラスのコンストラクタの呼び出しには call を使用 // 新聞としての追加プロパティ let page = "表紙"; // メソッド this.changePage = function(new_page) { page = new_page; console.log( this.getName() + "の" + page + "ページを開きました。" ); } this.readPage = function() { console.log( this.getName() + "の" + page + "ページを読みます。" ); } this.changeView = function(view) { this.changePage(view); this.readPage(); } } // インスタンス生成 let medias = { MaiasaNewspaper : new Newspaper("毎朝新聞"), LivingTelevision : new Television("リビングのテレビ"), TouzaiNewspaper : new Newspaper("東西新聞"), } // 連続実行 Object.keys(medias).forEach(function (key) { medias[key].getInformation(); var random = Math.floor( Math.random() * 20 ) + 1; // ページやチャンネルを乱数で指定 medias[key].changeView( random ); }); // 出力は以下のようになる。ページやチャンネルは乱数。 // 毎朝新聞で情報を得る。 // 毎朝新聞の2ページを開きました。 // 毎朝新聞の2ページを読みます。 // リビングのテレビを点けました。 // リビングのテレビの1チャンネルを観ます。 // リビングのテレビのチャンネルを5に変えました。 // リビングのテレビの5チャンネルを観ます。 // 東西新聞で情報を得る。 // 東西新聞の1ページを開きました。 // 東西新聞の1ページを読みます。
クラス
ここまでのソースコードはクラスを使えば(特に他の言語で慣れ親しんだ方であれば)もっと分かりやすくなります。
ただし、クラス構文はECMAScript 6(ES6)以降になるのでIEでは使えません。クラスのパブリック記述やプライベート記述となるとさらにSafariも未対応になるのでiPhoneでも使えなくなります。
最初の名前空間で作成したコードをクラスで書き直すと以下のようになります。
// 新聞 class Newspaper { // プロパティ name = "毎朝新聞"; page = "表紙"; // メソッド changePage(new_page) { this.page = new_page; console.log( this.name + "の" + this.page + "ページを開きました。" ); } readPage() { console.log( this.name + "の" + this.page + "ページを読みます。" ); } } // テレビ class Television { // プロパティ name = "リビングのテレビ"; power = "off"; channel = 1; // メソッド onPower() { if( this.power == "on" ) return; this.power = "on"; console.log( this.name + "を点けました。" ); } changeChannel(new_channel) { this.channel = new_channel; console.log( this.name + "のチャンネルを" + this.channel + "に変えました。" ); } watchChannel() { console.log( this.name + "の" + this.channel + "チャンネルを観ます。" ); } } // インスタンス生成 const MaiasaNewspaper = new Newspaper(); const LivingTelevision = new Television(); // 実行 MaiasaNewspaper.changePage( "テレビ欄" ); // 毎朝新聞のテレビ欄ページを開きました。 MaiasaNewspaper.readPage(); // 毎朝新聞のテレビ欄ページを読みます。 LivingTelevision.onPower(); // リビングのテレビを点けました。 LivingTelevision.changeChannel( 10 ); // リビングのテレビのチャンネルを10に変えました。 LivingTelevision.watchChannel(); // リビングのテレビの10チャンネルを観ます。 LivingTelevision.changeChannel( 4 ); // リビングのテレビのチャンネルを4に変えました。 LivingTelevision.watchChannel(); // リビングのテレビの4チャンネルを観ます。 LivingTelevision.changeChannel( 1 ); // リビングのテレビのチャンネルを1に変えました。 LivingTelevision.watchChannel(); // リビングのテレビの1チャンネルを観ます。クラスはオブジェクトの設計書みたいなものなので、そのままは使えません。
new
でインスタンス化する必要があります。クラスのメソッドにはfunction
宣言が不要になり、プロパティの宣言は変数同様に=
で値を入れます。クラスでのカプセル化
クラスのプライベートフィールド宣言はちょっと特殊で頭に
#
を付けます。
カプセル化のところで作成したコードをクラスで書き直すと以下のようになります。// テレビ class Television { // プロパティ #name = "リビングのテレビ"; #power = "off"; #channel = 1; // メソッド onPower() { if( this.#power == "on" ) return; this.#power = "on"; console.log( this.#name + "を点けました。" ); } changeChannel(new_channel) { // 新しいチャンネルを代入する前に値が正しいかチェック new_channel = parseInt(new_channel); if ( isNaN(new_channel) ) return; // チャンネルは数字意外は無視。 if( new_channel < 1 || new_channel > 100 ) return; // チャンネルは最低1、最高100。それ以外は無視。 this.#channel = new_channel; console.log( this.#name + "のチャンネルを" + this.#channel + "に変えました。" ); } watchChannel() { console.log( this.#name + "の" + this.#channel + "チャンネルを観ます。" ); } } // インスタンス生成 const LivingTelevision = new Television(); // 実行 LivingTelevision.onPower(); // 出力: リビングのテレビを点けました。 LivingTelevision.channel = -10; // プライベートなプロパティとは別のプロパティにアクセスしている。 // LivingTelevision.#channel = -10; // これはプライベートにはアクセスできないよと怒られる。 LivingTelevision.changeChannel( -10 ); // 誤ったチャンネル指定は無視される。 LivingTelevision.watchChannel(); // 出力: リビングのテレビの1チャンネルを観ます。 console.log(LivingTelevision.channel); // -10 と出力される。クラスでの継承
クラスの継承は他の言語でもおなじみの
extends
を使います。
継承のところで作成したコードをクラスで書き直すと以下のようになります。// メディア class Media { // プロパティ #name; // コンストラクタ constructor(media_name) { this.#name = media_name; } // ゲッター get name() { return this.#name; } // メソッド getInformation() // 各媒体ごとに異なる動作をするメソッドになる { console.log( this.name + "で情報を得る。" ); } } // テレビ class Television extends Media { // テレビとしての追加プロパティ #power = "off"; #channel = 1; // コンストラクタ constructor(media_name) { super(media_name); // 親コンストラクターを呼び出す } // メソッド onPower() { if( this.#power == "on" ) return; this.#power = "on"; console.log( this.name + "を点けました。" ); } changeChannel(new_channel) { // 新しいチャンネルを代入する前に値が正しいかチェック new_channel = parseInt(new_channel); if ( isNaN(new_channel) ) return; // チャンネルは数字意外は無視。 if( new_channel < 1 || new_channel > 100 ) return; // チャンネルは最低1、最高100。それ以外は無視。 this.#channel = new_channel; console.log( this.name + "のチャンネルを" + this.#channel + "に変えました。" ); } watchChannel() { console.log( this.name + "の" + this.#channel + "チャンネルを観ます。" ); } getInformation() // 親と同じメソッド名を定義することでオーバーライドする { this.onPower(); // 情報を得るためにはまずは電源を入れる this.watchChannel(); } } // 新聞 class Newspaper extends Media { // 新聞としての追加プロパティ #page = "表紙"; // コンストラクタ constructor(media_name) { super(media_name); // 親コンストラクターを呼び出す } // メソッド changePage(new_page) { this.#page = new_page; console.log( this.name + "の" + this.#page + "ページを開きました。" ); } readPage() { console.log( this.name + "の" + this.#page + "ページを読みます。" ); } } // インスタンス生成 const MaiasaNewspaper = new Newspaper("毎朝新聞"); const LivingTelevision = new Television("リビングのテレビ"); // 実行 MaiasaNewspaper.getInformation(); // 毎朝新聞で情報を得る。 LivingTelevision.getInformation(); // リビングのテレビを点けました。リビングのテレビの1チャンネルを観ます。 console.log(MaiasaNewspaper.name); // 毎朝新聞 console.log(LivingTelevision.name); // リビングのテレビ関数で記述していた方では
new
したときに引数を渡し、その引数をそのままプロパティに代入していましたが、クラスではnew
したときに必ず実行されるコンストラクタを用意します。継承先で親のコンストラクタを呼び出したい場合はスーパークラス
super
を呼び出します。親クラスにあるゲッターは、外部から
MaiasaNewspaper.name
というようにプロパティのように参照でき、内部ではプロパティではなくメソッドを実行できるので、よりカプセル化しやすくなります。最後にポリモーフィズムのところで作成したコードをクラスで書き直すと以下のようになります。
// メディア class Media { // プロパティ #name; // コンストラクタ constructor(media_name) { this.#name = media_name; } // ゲッター get name() { return this.#name; } // メソッド getInformation() // 各媒体ごとに異なる動作をするメソッドになる { console.log( this.name + "で情報を得る。" ); } changeView(view) { // 各オブジェクト側で実装してね } } // テレビ class Television extends Media { // テレビとしての追加プロパティ #power = "off"; #channel = 1; // コンストラクタ constructor(media_name) { super(media_name); // 親コンストラクターを呼び出す } // メソッド onPower() { if( this.#power == "on" ) return; this.#power = "on"; console.log( this.name + "を点けました。" ); } changeChannel(new_channel) { // 新しいチャンネルを代入する前に値が正しいかチェック new_channel = parseInt(new_channel); if ( isNaN(new_channel) ) return; // チャンネルは数字意外は無視。 if( new_channel < 1 || new_channel > 100 ) return; // チャンネルは最低1、最高100。それ以外は無視。 this.#channel = new_channel; console.log( this.name + "のチャンネルを" + this.#channel + "に変えました。" ); } watchChannel() { console.log( this.name + "の" + this.#channel + "チャンネルを観ます。" ); } getInformation() // 親と同じメソッド名を定義することでオーバーライドする { this.onPower(); // 情報を得るためにはまずは電源を入れる this.watchChannel(); } changeView(view) { this.changeChannel(view); this.watchChannel(); } } // 新聞 class Newspaper extends Media { // 新聞としての追加プロパティ #page = "表紙"; // コンストラクタ constructor(media_name) { super(media_name); // 親コンストラクターを呼び出す } // メソッド changePage(new_page) { this.#page = new_page; console.log( this.name + "の" + this.#page + "ページを開きました。" ); } readPage() { console.log( this.name + "の" + this.#page + "ページを読みます。" ); } changeView(view) { this.changePage(view); this.readPage(); } } // インスタンス生成 let medias = { MaiasaNewspaper : new Newspaper("毎朝新聞"), LivingTelevision : new Television("リビングのテレビ"), TouzaiNewspaper : new Newspaper("東西新聞"), } // 連続実行 Object.keys(medias).forEach(function (key) { medias[key].getInformation(); var random = Math.floor( Math.random() * 20 ) + 1; // ページやチャンネルを乱数で指定 medias[key].changeView( random ); }); // 出力は以下のようになる。ページやチャンネルは乱数。 // 毎朝新聞で情報を得る。 // 毎朝新聞の2ページを開きました。 // 毎朝新聞の2ページを読みます。 // リビングのテレビを点けました。 // リビングのテレビの1チャンネルを観ます。 // リビングのテレビのチャンネルを5に変えました。 // リビングのテレビの5チャンネルを観ます。 // 東西新聞で情報を得る。 // 東西新聞の1ページを開きました。 // 東西新聞の1ページを読みます。
- 投稿日:2021-01-17T09:42:09+09:00
リーダブルテストコード
はじめに
よく言われるように、ソースコードというものは書かれることよりも読まれることの方が多く、それゆえ読みやすいコードを書くということが非常に重要です。それはテストコードにおいても同様であり、プロダクトコードと同等に資産として扱う必要があります。
テストコードは具体的な値を用いて記述し、また複数の変数の値の組み合わせでテストケースを起こすため、プロダクトコードと比べて冗長になりがちです。
書籍『リーダブルコード』の14章でもテストコードの読みやすさについて触れられていますが、本稿では読みづらいテストコードをリファクタリングして読みやすくするためのテクニックを紹介したいと思います。
なおサンプルコードはJavaScriptで記述されており、そのテストコードはJest1を用いて書いています。
ソースコードはGitHubにあります。リファクタリング(その壱)
以下の、決して読みやすいとはいえないテストコードをリファクタリングしていきます。
Scrumによるアジャイル開発プロセスを支援するソフトウェアの一部だと思ってください。HardToReadTest.spec.jsdescribe('Sprint', () => { it('ストーリーが正しい', () => { const sprint = new Sprint(1); expect(sprint.id).toBe(1); expect(sprint.stories).toHaveLength(0); const story = new Story('環境構築', '開発環境をセットアップする'); sprint.addStory(story); expect(sprint.stories).toHaveLength(1); }); it('アサイン状況を正しく取得できる', () => { const sprint = new Sprint(1); const story1 = new Story('環境構築', '開発環境をセットアップする', 3, '井上'); sprint.addStory(story1); const story2 = new Story('サンプル開発', 'サンプルコードを書く', 2, '山田'); sprint.addStory(story2); const story3 = new Story('ユニットテスト', 'テストコードを書く', 1, '町田'); sprint.addStory(story3); const story4 = new Story('E2E', 'E2Eテストを作成する', 2, '山田'); sprint.addStory(story4); // 人別に、ポイント合計の降順でソートされる expect(sprint.assignment).toEqual( ['山田', 4], ['井上', 3], ['町田', 1], ]); }); });テスト対象を明確化する
テストにおける検証対象コンポーネントをSUT(System Under Test)と呼びます。一方、SUTが依存するコンポーネントをDOC(Depended-on Component)と呼びます。
テストケースにおける主人公であるSUTをその他の登場人物と明確に区別するため、SUTを格納する変数をsut
と命名するのはよいプラクティスです。describe('Sprint', () => { it('ストーリーが正しい', () => { const sut = new Sprint(1); expect(sut.id).toBe(1); expect(sut.stories).toHaveLength(0); const story = new Story('環境構築', '開発環境をセットアップする'); sut.addStory(story); expect(sut.stories).toHaveLength(1); }); ... })テストケースではただ一つのことを検証する
一つのテストケースにいろいろなことを盛り込まないようにしましょう。先ほどのテストケースは2つに分割します。
describe('Sprint', () => { it('初期状態が正しい', () => { const sut = new Sprint(1); expect(sut.id).toBe(1); expect(sut.stories).toHaveLength(0); }); it('ストーリーを追加できる', () => { const sut = new Sprint(1); const story = new Story('環境構築', '開発環境をセットアップする'); sut.addStory(story); expect(sut.stories).toHaveLength(1); }); });共通するテストフィクスチャをセットアップメソッドで作成する
テストフィクスチャとはテストの事前条件を指します。具体的にはSUTの状態、DOCの状態、環境の状態などのことです。
コードの重複を取り除くため、テストフィクスチャを作成するコードはセットアップメソッドへ移動させましょう。Jestの場合、beforeEach
メソッドとなります。describe('Sprint', () => { let sut; beforeEach(() => { sut = new Sprint(1); }); it('初期状態が正しい', () => { expect(sut.id).toBe(1); expect(sut.stories).toHaveLength(0); }); it('ストーリーを追加できる', () => { const story = new Story('環境構築', '開発環境をセットアップする'); sut.addStory(story); expect(sut.stories).toHaveLength(1); }); ... });AAA(またはGive-When-Then)を意識する
テストコードは以下の4つのフェーズから成り立ちます。
1. 準備
2. 実行
3. 検証
4. 後片付け4つ目の後片付けが必要となるのはDB・ストレージ・ネットワーク等の共有リソースを使用するテストケースが中心ですので、一般的なテストケースでは準備・実行・検証が基本的な構成要素となります。
AAA(トリプルエー)とは、この3つをそれぞれArrange/Act/Assertと呼んでその頭文字を取ったものです。BDD2流ならばGiven-When-Thenと呼ぶでしょう。
テストコードを書く際はこの3つを意識し、コメントを入れて論理分割すると見通しがよくなります。describe('Sprint', () => { ... it('ストーリーを追加できる', () => { // Arrange const story = new Story('環境構築', '開発環境をセットアップする'); // Act sut.addStory(story); // Assert expect(sut.stories).toHaveLength(1); }); })振舞いに影響を与える値とそうでない値を区別する
以下のテストケースを見ましょう。
describe('Sprint', () => { ... it('アサイン状況を正しく取得できる', () => { const sprint = new Sprint(1); const story1 = new Story('環境構築', '開発環境をセットアップする', 3, '井上'); sprint.addStory(story1); const story2 = new Story('サンプル開発', 'サンプルコードを書く', 2, '山田'); sprint.addStory(story2); const story3 = new Story('ユニットテスト', 'テストコードを書く', 1, '町田'); sprint.addStory(story3); const story4 = new Story('E2E', 'E2Eテストを作成する', 2, '山田'); sprint.addStory(story4); // 人別に、ポイント合計の降順でソートされる expect(sprint.assignment).toEqual([ ['山田', 4], ['井上', 3], ['町田', 1], ]); }); });
Story
は複数の属性を持ちます(タイトル、記述、ポイント、担当者)が、この中でテスト対象の振舞いであるsprint.assignment
(アサイン状況)に影響を与えるのはポイントと担当者です。それ以外の属性の値はこのテストケースにおいてはノイズとなります。
このような属性値には'any'
といった値を設定することで、本当に関心のある属性値を際立たせることが可能です。describe('Sprint', () => { ... it('アサイン状況を正しく取得できる', () => { // Arrange const story1 = new Story('any', 'any', 3, '井上'); sut.addStory(story1); const story2 = new Story('any', 'any', 2, '山田'); sut.addStory(story2); const story3 = new Story('any', 'any', 1, '町田'); sut.addStory(story3); const story4 = new Story('any', 'any', 2, '山田'); sut.addStory(story4); // Act const assignment = sut.assignment; // Assert // 人別に、ポイント合計の降順でソートされる expect(assignment).toEqual([ ['山田', 4], ['井上', 3], ['町田', 1], ]); }); });生成メソッドを活用する
しかしながら、
'any'
という値自体も少し邪魔ですね。new
演算子で直接オブジェクトを生成するのではなく、オブジェクトを生成するヘルパーメソッドを用意しましょう。
Story
オブジェクトを生成する関数を定義します。const defaults = (val, defaultVal) => val === undefined ? defaultVal : val; const aStory = ({title, description, point, asignee} = {}) => new Story( defaults(title, 'any'), defaults(description, 'any'), defaults(point, 0), defaults(asignee, 'any') );引数に指定したオブジェクトに
Story
コンストラクタの属性がある場合はそれを、そうでなければデフォルト値を使ってStory
オブジェクトを生成するように実装しています。
このヘルパーメソッドを利用すると、前述のテストケースは以下のように書き直すことができます。describe('Sprint', () => { ... it('アサイン状況を正しく取得できる', () => { // Arrange const story1 = aStory({point: 3, asignee: '井上'}); const story2 = aStory({point: 2, asignee: '山田'}); const story3 = aStory({point: 1, asignee: '町田'}); const story4 = aStory({point: 2, asignee: '山田'}); [story1, story2, story3, story4].forEach(s => sut.addStory(s)); // Act const assignment = sut.assignment; // Assert // 人別に、ポイント合計の降順でソートされる expect(assignment).toEqual([ ['山田', 4], ['井上', 3], ['町田', 1], ]); }); });ノイズが減り、少し読みやすくなったのではないでしょうか。
計算結果ではなく計算式を記述する
先ほどのコードの
Assert
の部分を抜き出します。expect(assignment).toEqual([ ['山田', 4], ['井上', 3], ['町田', 1], ]);山田さんの4(ポイント)は、
Arrange
でセットした2つのストーリーのポイントの合計値です。const story1 = aStory({point: 3, asignee: '井上'}); const story2 = aStory({point: 2, asignee: '山田'}); // ←これ const story3 = aStory({point: 1, asignee: '町田'}); const story4 = aStory({point: 2, asignee: '山田'}); // ←これですが、その情報はテストコードからは失われています。以下のように、計算した結果ではなく計算式として記述することで意図がより明確となります。
expect(assignment).toEqual([ ['山田', (2 + 2)], // 計算式で記述する ['井上', 3], ['町田', 1], ]);さらに、以下のように定数を使用することでもっと見通しがよくなるでしょう。
it('アサイン状況を正しく取得できる', () => { // Arrange const [pt1, pt2, pt3, pt4] = [3, 2, 1, 2]; // 定数化 const story1 = aStory({point: pt1, asignee: '井上'}); const story2 = aStory({point: pt2, asignee: '山田'}); const story3 = aStory({point: pt3, asignee: '町田'}); const story4 = aStory({point: pt4, asignee: '山田'}); [story1, story2, story3, story4].forEach(s => sut.addStory(s)); // Act const assignment = sut.assignment; // Assert // 人別に、ポイント合計の降順でソートされる expect(assignment).toEqual([ ['山田', (pt2 + pt4)], ['井上', pt1], ['町田', pt3], ]); });テストケース名を雄弁にする
先ほどのテストケースの、以下のコメントについて。
// 人別に、ポイント合計の降順でソートされる expect(assignment).toEqual([ ['山田', (pt2 + pt4)], ['井上', pt1], ['町田', pt3], ]);これは、テストケース名でちゃんと表現してあげましょう。
it('アサイン状況は人別にポイントが表示され、ポイント合計の降順でソートされる', () => { // Arrange const [pt1, pt2, pt3, pt4] = [3, 2, 1, 2]; const story1 = aStory({point: pt1, asignee: '井上'}); const story2 = aStory({point: pt2, asignee: '山田'}); const story3 = aStory({point: pt3, asignee: '町田'}); const story4 = aStory({point: pt4, asignee: '山田'}); [story1, story2, story3, story4].forEach(s => sut.addStory(s)); // Act const assignment = sut.assignment; // Assert expect(assignment).toEqual([ ['山田', (pt2 + pt4)], //4ポイント ['井上', pt1], // 3ポイント ['町田', pt3], // 1ポイント ]); });リファクタリング(その弐)
Story
に対する以下のテストコードをリファクタリングします。describe('Story', () => { it('未アサインは開始不可', () => { const story = new Story('環境構築', '開発環境をセットアップする', 3, null); expect(story.canBeStarted).toBe(false); }); it('ポイントを振ってない場合は開始不可', () => { const story = new Story('環境構築', '開発環境をセットアップする', 0, '井上'); expect(story.canBeStarted).toBe(false); }); it('ポイントもアサインも入ってない場合は開始不可', () => { const story = new Story('環境構築', '開発環境をセットアップする', 0, null); expect(story.canBeStarted).toBe(false); }); it('ポイントもアサインも入っている場合は開始可', () => { const story = new Story('環境構築', '開発環境をセットアップする', 3, '井上'); expect(story.canBeStarted).toBe(true); }); });いったん、これまでに述べたリファクタリングテクニックを適用します。
describe('Story', () => { it('未アサインは開始不可', () => { // Arrange const sut = aStory({point: 3, asignee: null}); // Act const canBeStarted = sut.canBeStarted; // Assert expect(canBeStarted).toBe(false); }); it('ポイントを振ってない場合は開始不可', () => { // Arrange const sut = aStory({point: 0, asignee: '山田'}); // Act const canBeStarted = sut.canBeStarted; // Assert expect(canBeStarted).toBe(false); }); it('ポイントもアサインも入ってない場合は開始不可', () => { // Arrange const sut = aStory({point: 0, asignee: null}); // Act const canBeStarted = sut.canBeStarted; // Assert expect(canBeStarted).toBe(false); }); it('ポイントもアサインも入っている場合は開始可', () => { // Arrange const sut = aStory({point: 3, asignee: '山田'}); // Act const canBeStarted = sut.canBeStarted; // Assert expect(canBeStarted).toBe(true); }); });パラメータ化テストを適用する
前述の4つのテストケースはとても似通っていて冗長な印象があります。パラメータ化テスト(Parameterized Test)というテスティングパターンを使ってすっきりまとめてみましょう。
describe('Story', () => { test.each` point | asignee | expected | desc ${3} | ${null} | ${false} | ${'未アサインは開始不可'} ${0} | ${'山田'} | ${false} | ${'ポイントを振ってない場合は開始不可'} ${0} | ${null} | ${false} | ${'ポイントもアサインも入ってない場合は開始不可'} ${3} | ${'山田'} | ${true} | ${'ポイントもアサインも入っている場合は開始可'} `("開始可能か: $desc", ({point, asignee, expected}) => { // Arrange const sut = aStory({point, asignee}); // Act const canBeStarted = sut.canBeStarted; // Assert expect(canBeStarted).toBe(expected); }); })
test.each
の部分はタグ付きテンプレートリテラル3を使って記述しており、Jestが提供するDSL4です。テンプレートリテラル中の2行目以降の各行がテストケースに展開されます。
例えば2行目は以下のテストケースと同等ということになります。// point = 3, asignee = null, expected = false it('開始可能か: 未アサインは開始不可', () => { // Arrange const sut = aStory({point: 3, asignee: null}); // Act const canBeStarted = sut.canBeStarted; // Assert expect(canBeStarted).toBe(false); });最終的なコード
リファクタリング後のテストコードは以下となります。
ReadableTest.spec.jsconst {Sprint, Story} = require('../entities'); const defaults = (val, defaultVal) => val === undefined ? defaultVal : val; const aSprint = ({id, description} = {}) => new Sprint( defaults(id, 1), defaults(description, 'any') ); const aStory = ({title, description, point, asignee} = {}) => new Story( defaults(title, 'any'), defaults(description, 'any'), defaults(point, 0), defaults(asignee, 'any') ); describe('Sprint', () => { let sut; beforeEach(() => { sut = aSprint(); }); it('初期状態が正しい', () => { // Assert expect(sut.id).toBe(1); expect(sut.stories).toHaveLength(0); }); it('ストーリーを追加できる', () => { // Arrange const story = new Story('環境構築', '開発環境をセットアップする'); // Act sut.addStory(story); // Assert expect(sut.stories).toHaveLength(1); }); it('アサイン状況は人別にポイントが表示され、ポイント合計の降順でソートされる', () => { // Arrange const [pt1, pt2, pt3, pt4] = [3, 2, 1, 2]; const story1 = aStory({point: pt1, asignee: '井上'}); const story2 = aStory({point: pt2, asignee: '山田'}); const story3 = aStory({point: pt3, asignee: '町田'}); const story4 = aStory({point: pt4, asignee: '山田'}); [story1, story2, story3, story4].forEach(s => sut.addStory(s)); // Act const assignment = sut.assignment; // Assert expect(assignment).toEqual([ ['山田', (pt2 + pt4)], //4ポイント ['井上', pt1], // 3ポイント ['町田', pt3], // 1ポイント ]); }); }); describe('Story', () => { test.each` point | asignee | expected | desc ${3} | ${null} | ${false} | ${'未アサインは開始不可'} ${0} | ${'山田'} | ${false} | ${'ポイントを振ってない場合は開始不可'} ${0} | ${null} | ${false} | ${'ポイントもアサインも入ってない場合は開始不可'} ${3} | ${'山田'} | ${true} | ${'ポイントもアサインも入っている場合は開始可'} `("開始可能か: $desc", ({point, asignee, expected}) => { // Arrange const sut = aStory({point, asignee}); // Act const canBeStarted = sut.canBeStarted; // Assert expect(canBeStarted).toBe(expected); }); });参考までに、プロダクトコードは以下となります。
entities.jsclass Sprint { constructor(id, description) { this.id = id; this.description = description; this.stories = []; } addStory (story) { this.stories.push(story); } get assignment () { const perAsignee = this.stories.reduce((map, story) => { const key = story.asignee; if (map.has(key)) { map.set(key, map.get(key) + story.point); } else { map.set(key, story.point); } return map; }, new Map()); return Array.from(perAsignee).sort((s1, s2) => s2[1] - s1[1]); } } class Story { constructor(title, description, point = 0, asignee) { this.title = title; this.description = description; this.point = point; this.asignee = asignee; } get canBeStarted () { return Boolean(this.asignee) && this.point > 0; } } module.exports.Sprint = Sprint; module.exports.Story = Story;まとめ
テストコードが散らかっていき可読性が低下すると、テストコードの信頼性が失われ、ゆくゆくはメンテナンスされなくなってしまうリスクがあります。テストコードの負債化は、プロダクトコードの品質やメンテナンス性に大きな影響を与える由々しき問題です。
そのような状況の発生を防止するため、テストコードの可読性の大切さを知り、本稿で紹介したようなテクニックを用いて日々リファクタリングをしていくことを心がけましょう。参考文献
- Dustin Boswell, Trevor Foucher 『The Art of Readable Code』 (O'REILLY, 2011)
- Gerard Meszaros 『xUnit Test Patterns』 (Addison-Wesley, 2007)
- 投稿日:2021-01-17T08:11:27+09:00
【JavaScript】ストップウォッチを作ってみた
概要
何だかんだでよく使うので自分でコピペする為に作った。
丸コピしてくれてもいいし、クソコードと反面教師にしてもいい。動作サンプル
https://jsfiddle.net/mahny/wdkzjf8v/105/
機能
- ラップがないストップウォッチと同等(スタート、ストップ、リセット)
- DIV等の任意のタグに計測時間の表示を行う
- 計測時間の更新間隔をms単位で設定できる
- スタート/ストップの精度とは別なので例え10秒間隔にしてもミリ秒で時間を記録する
使い方
最低限、時間表示の為のタグとスタート兼ストップボタン、リセットボタンが必要。
<div id="stopwatch">00:00:00.000</div> <button onclick="startStop()">START/STOP</button> <button onclick="reset()">RESET</button>実行する場合は、インスタンスを作って、
startOrStop()
とreset()
を呼ぶためのハンドラを設ければいい。let sw = new StopWatch('stopwatch', 50); function startStop() { sw.startOrStop(); } function reset() { sw.reset(); }コード
/** * ストップウォッチクラス * @auther mahny * @copyright mahny * @license MIT License */ class StopWatch { /** * 指定したIDを持つタグに対してストップウォッチ処理を行う * * @params {string} displayId * 時間表示を行うタグのID * @params {number} interval * タイマー表示の更新間隔(ms) * @throws 指定したIDを持つタグが見つからない時 * @throws 指定した更新間隔が正の数値以外の時 */ constructor(displayId, interval = 100) { this._displayElement = document.getElementById(displayId); if (!this._displayElement) { throw "指定したIDを持つタグが見つからない。 / displayId=[" + displayId + "]"; } if ((typeof(interval) !== 'number') || interval < 1) { throw "指定した更新間隔は正の数値ではない。 / interval=[" + interval + "]"; } this._interval = Math.floor(interval); this._startTime = 0; this._totalTime = 0; } /** * ストップウォッチの状態を取得する * * @returns {string} ストップウォッチの状態 */ getStatus() { if (0 < this._startTime) { return 'start'; } else if (0 < this._totalTime) { return 'stop'; } else { return 'init'; } } /** * 時間表示タグの表示時間を現在値に更新する */ _refresh() { if (!this._displayElement) { return; } let result = '00:00:00.000'; let restTotalTime = 0; switch (this.getStatus()) { case 'start': restTotalTime = this._totalTime + (new Date().getTime() - this._startTime); break; case 'stop': restTotalTime = this._totalTime; break; default: // nop } let hour = Math.floor(restTotalTime / 3600000); let min = Math.floor(restTotalTime / 60000) % 3600; let sec = Math.floor(restTotalTime / 1000) % 60; let msec = restTotalTime % 1000; result = ('0' + hour).slice(-2) + ':' + ('0' + min).slice(-2) + ':' + ('0' + sec).slice(-2) + '.' + ('00' + msec).slice(-3); this._displayElement.innerHTML = result; } /** * 計測を開始または再開する */ _start() { // 画面更新用関数(ループ) let self = this; let masureTimeFunc = function() { if (self.getStatus() === 'start') { self._refresh(); setTimeout(masureTimeFunc, self._interval); } } this._startTime = new Date().getTime(); setTimeout(masureTimeFunc, this._interval); } /** * 計測を停止する */ _stop() { this._totalTime += new Date().getTime() - this._startTime; this._startTime = 0; this._refresh(); } /** * 状態によって、計測を開始/再開または停止をする */ startOrStop() { switch (this.getStatus()) { case 'start': this._stop(); break; default: this._start(); } } /** * 状態によって、計測時間をリセットを行う */ reset() { let status = this.getStatus(); if (status === 'stop') { this._startTime = 0; this._totalTime = 0; this._refresh(); } else { console.log('停止状態でない為、リセットできません。 / status=[' + status + ']'); } } }
- 投稿日:2021-01-17T04:54:58+09:00
MySQLで取得したデータを、EJSでWebブラウザに出力する
はじめに
こちらは、エンジニアの新たな学びキャンペーンに向けた記事となります。
Node.js + Express で作る Webアプリケーション 実践講座を参考にしながら、
データベース(以下DB)内のデータを、Webブラウザに表示する方法を記事にしました。なお、ここではNode.jsのテンプレートエンジンであるEJSと、RDB(リレーショナルデータベース)であるMariaDB(MySQL)を利用します。
(上記講座ではMongoDBが利用されていますが、本記事ではMySQLに置き換えました。)
実行環境
- Node.js v12.16.3
- Express 4.16.1
- 10.4.11-MariaDB
対象者
- JavaScriptの文法自体は学んだけど、Web技術はまだほとんど学べていない人
- かんたんなCRUD操作に関するSQLを理解している人
本記事でわかること
- サーバーサイド言語Node.js & フレームワークExpressを使い、Hello Worldする方法
- ExpressとMariaDB(MySQL)の連携方法
- ExpressとMariaDB(MySQL)を使い、DB内のデータをWebブラウザ上に出力する方法
対象のUdemy講座で学んだこと
対象の講座で学んだことのうち、特に本記事へ反映する内容は以下となります。
- パッケージマネージャであるnpmを使い、ミドルウェアやフレームワークを導入する方法
- EJSの構文の使い方
- Expressでのルーティング方法(特に関数の引数に関して)
EJSとは
EJSは、テンプレートエンジンと呼ばれるもののひとつで、
テンプレートエンジンはHTMLの中にプログラム言語を埋め込むことができます。特にEJSにおいては、HTML文書の中に
<% %>
,<%= %>
タグなどを埋め込み、この中にプログラムを記述します。EJSの基本構文
EJSの基本的な書き方にきれいにまとまっていたので、
こちらを参照すると幸せになれます。EJSの利点
EJSは、サーバーサイドで保持している変数の値を併用してHTMLを記述するとき、書きやすさ・読みやすさの点で非常に強力です。
例えばサーバーサイド言語のみでHTML文書を書く場合、次のようなソースコードになります。
app.jsconst express = require("express"); const app = express(); app.get("/", (req, res) => { const text = "Hello World"; let data = "<!DOCTYPE html>\r\n"; data += "<html>\r\n"; data += " <head>\r\n"; data += " <meta charset='UTF-8'>\r\n"; data += " <title>hoge</title>\r\n"; data += " </head>\r\n"; data += " <body>\r\n"; data += "<p>"; data += text data += "</p>\r\n"; data += " </body>\r\n"; data += "</html>"; res.send(data); }); app.listen(3000);一方で、テンプレートエンジンを併用した場合は、次のようになります。
クォーテーションや改行を意味する\r\n
などが消え、読み書きしやすくなっているのが分かります。app.jsconst express = require("express"); const app = express(); app.set("view engine", "ejs"); app.get("/", (req, res) => { const text = "Hello World"; res.render("index", {text}); }); app.listen(3000);index.ejs<!DOCTYPE html> <html> <head> <meta charset='UTF-8'> <title>hoge</title> </head> <body> <p><%= text %></p> </body> </html>実行の準備
さて、まずは環境の構築を行います。
Node.js, MariaDB(MySQL)はインストールされているものとします。サーバーサイドの準備
以下のコマンドを順に実行して、フレームワークやミドルウェアを導入します。
$ npm init
$ npm install express --save
,$ npm install mysql
$ npm install ejs -- save
MariaDB(MySQL)の準備
以下のSQLを順に実行します。
この操作により、Webサイトの名前・URLに関するテーブルを作成し、データの追加も行います。
create database website
create table website(name varchar(255), url varchar(255));
insert into website(name, url) values ("google", "https://www.google.com/"), ("amazon", "https://www.amazon.co.jp/"), ("apple", "https://www.apple.com/"), ("facebook", "https://www.facebook.com/");
実装する
次のような順序で、簡単なことから実装していきます。
- EJSを使い、Webブラウザ上でHello Worldする
- DB接続を行いデータを取り出し、ターミナル上にデータを出力する
- DB接続を行い、EJSファイルを利用してWebブラウザ上にデータを出力する(ここでは一度失敗してみる)
- DB接続を行い、EJSファイルを利用してWebブラウザ上にデータを出力する(再チャレンジし、成功する)
1. EJSでHello Worldしてみよう
まずはDBのことは一旦 忘れて、EJSを使ってHello Worldをしてみます。
下記ソースコードを保存後、ターミナル上で
$ node app.js
と入力し、
Webブラウザでhttp://localhost:3000/
にアクセスします。次の画像のように表示されたら成功です。
ちなみに、
app.js
のソースコード内のapp.get()
の第一引数はリクエストURL、
第二引数はリクエストが送られたときに実行されるコールバック関数を指します。この処理を口語的に説明するなら、
http://localhost:3000/
が呼ばれたら次のコールバック関数を実行してね!
そしてそのコールバック関数には、index.ejsを表示して!っていう命令も含まれてるよ!
といったところでしょうか。実行するソースコード
app.jsconst express = require("express"); const app = express(); app.set("view engine", "ejs"); app.get("/", (req, res)=>{ res.render("index.ejs"); // デフォルトでは /viewsからの相対パスで表すので注意 }) app.listen(3000);views/index.ejs<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <h1>Hello World</h1> </body> </html>2. DBに接続してみよう
次に、DB内のデータをターミナル上で表示する実装を行います。
app.js
を次のように書き換えます。
なお、mysql.createConnection()
の各種ユーザ情報は、必要に応じて書きかえてください。実行するソースコード
app.jsconst express = require("express"); const app = express(); const mysql = require("mysql"); const connection = mysql.createConnection({ host: "localhost", user: "root", password: "1234", database: "website" }); app.set("view engine", "ejs"); app.get("/", (req, res)=>{ const sql = "select * from website"; connection.query(sql, (err, result, fields)=>{ if(err)throw err; console.log(result); }) res.render("index.ejs"); // デフォルトでは "/views"ディレクトリからの相対パスで表すので注意 }) app.listen(3000);ターミナル上で
$ node app.js
と入力し、Webブラウザでhttp://localhost:3000/
にアクセスします。
前回と同様にHello WorldがWebブラウザに表示されており、
更にターミナル上に、次のような表示があれば成功です。ここでは
console.log(result)
の実行による、ターミナルのデータ出力結果から、
変数result
にはDBから取り出したデータが入っていることが確認できます。$ node app.js [ RowDataPacket { name: 'google', url: 'https://www.google.com/' }, RowDataPacket { name: 'amazon', url: 'https://www.amazon.co.jp/' }, RowDataPacket { name: 'apple', url: 'https://www.apple.com/' }, RowDataPacket { name: 'facebook', url: 'https://www.facebook.com/' } ]3. DB内のデータをWebブラウザに表示してみる
『1.EJSでHello Worldしてみよう』, 『2.DBに接続してみよう』では、
res.render("index.ejs")
とレンダリング先を表記し、ルーティングを設定していました。今回はレンダリング先にデータを渡すために、
app.get()
内にレンダリング先だけではなく、DBから取得したデータも記述する必要があります。
そこでres.render()
の第二引数に、レンダリング先に送るデータを記述します。より具体的にはいえば、
『2. DBに接続してみよう』で、変数result
にDBのデータが格納されることが確認できていました。
このresultをres.render()
の第二引数に指定します。
従って、ここではres.render("index", { web: result})
と記述します。ところで
{ web: result}
という記述に、ややこしさを感じるかもしれません。
これはresultからwebへ名前を置換してから、データを送るという処理を含んでいます。EJSに対して、resultという変数名をそのままに渡してしまうと、
「result?結果?いや何の結果を表す変数なのか、なんのこっちゃわからん」と、
フロントエンドエンジニアが困惑することになってしまいます。(webという変数名ならば適切なのかという問題はさておき。)
実行するソースコード
DBに保存しているWebサイト名やURLをWebブラウザ上に出力するため、
app.js
とindex.ejs
を、それぞれ次のように書き換えます。app.jsconst express = require("express"); const app = express(); const mysql = require("mysql"); const connection = mysql.createConnection({ host: "localhost", user: "root", password: "1234", database: "website" }); app.set("view engine", "ejs"); app.get("/", (req, res)=>{ const sql = "select * from website"; connection.query(sql, (err, result, fields)=>{ if(err)throw err; console.log(result); res.render("index", { web: result}); }) }) app.listen(3000);/views/index.ejs<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <h1>Hello World</h1> <%= web %> </body> </html>ターミナルに
node app.js
を入力し、Webブラウザでhttp://localhost:3000/
にアクセスします。
次の画像のような、Hello WorldとObjectという文字の羅列が確認できるでしょうか?
半分だけ成功です!本来であれば
Apple
といったWebサイト名や、
https://www.google.com/
のようなURLがほしかったところですが、
DBから何らかのデータを取り出すことには、ひとまず成功したのではないでしょうか。
[]
で囲まれたよくわからないものは4つで、DBに登録したレコードもちょうど4つでしたしね。次の項で、この問題を解決します。
4. DB内のデータをWebブラウザに表示してみる(再挑戦)
前項ではDBから、どうやら何らかのデータを取り出すことには成功しましたが、
Webサイトの名前やURLを取得することはできませんでした。この原因について考えます。
http://localhost:3000/
にアクセスしたとき、
console.log(result)
の実行によって、ターミナルに何か表示されたことは覚えているでしょうか?それは、次のような内容でした。
$ node app.js [ RowDataPacket { name: 'google', url: 'https://www.google.com/' }, RowDataPacket { name: 'amazon', url: 'https://www.amazon.co.jp/' }, RowDataPacket { name: 'apple', url: 'https://www.apple.com/' }, RowDataPacket { name: 'facebook', url: 'https://www.facebook.com/' } ]一見するとオブジェクトですが、
出力されたデータは[]
で囲まれているため、これはオブジェクトたちを格納している『配列』です。従って、例えば
web[0]["name"]
と記述します。実行するソースコード
例として、
https://www.google.com/
をWebブラウザ上に表示してみます。
次のようにindex.ejs
を書き換え、再度$ node app.js
で実行します。
下記画像のように、Webサイト名とURLが表示されたら、成功です。/views/index.ejs<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <h1>Hello World</h1> <%= web[0]["name"] %> <br> <%= web[0]["url"] %> </body> </html>この他にも応用として、
for文を利用するなどして、一度に複数のデータを出力すること、
あるいはSQLを変更してデータの更新・追加・削除することも可能です。おわりに
プログラミングを本格的に始めて1年も満たない未熟者の言葉ではありますが、
プログラミング言語の文法を修めるだけでは、Webのシステム開発は正直 不可能です。私はJavaScriptの文法を学習した後、サーバーサイド言語としてのJavaScript(Node.js)に入門したのですが、
すぐにHTTPリクエスト、ルーティングなどといったWeb特有の専門用語に悩まされました。Webアプリケーションのサーバーサイドへ入門する前に、
『この一冊で全部わかるWeb技術の基本』, 『Webを支える技術』, 『Web技術速習テキスト』といったWeb周りの情報に触れておくことを、強くおすすめします。参考