- 投稿日:2021-01-29T22:20:04+09:00
音のあるプッシュ通知を実装してみた(React.js)
はじめに
最近、個人開発でプッシュ通知を実装して、音をつけたいなと思ったので実装してみました。プッシュ通知はNotificationAPIを使っているので、残念ながらサイトを開いてないと通知されません。ServiceWorkerとweb-push使って、バックグラウンドで実装できるようにしたいと思ってます。
React使ってます。効果音探しに使わせていただきました↓
フリー音楽素材魔王魂ソースコード
説明は簡単にコメントで記述しました!
src/App.tsximport { useEffect, useState } from "react"; function App() { const [hasSound, setHasSound] = useState(true); useEffect(() => { if ("Notification" in window) { // 通知が許可されていたら早期リターン const permission = Notification.permission; if (permission === "denied" || permission === "granted") { return; } // 通知の許可を求める Notification.requestPermission().then(() => new Notification("テスト")); } }, []); const handlePushNotif = () => { if ("Notification" in window) { const notif = new Notification("こんにちは!"); // プッシュ通知が表示された時に起きるイベント notif.addEventListener("show", () => { // 状態によって音の有無を変える if (hasSound) { // 音再生 new Audio("./push.wav").play(); } }); } }; return ( <div> <button onClick={handlePushNotif}>PUSH</button> <button onClick={() => setHasSound((prev) => !prev)}> {hasSound ? "音なしにする" : "音ありにする"} </button> </div> ); } export default App;初回アクセス時は下の画像のようなものが出てくるので許可するとプッシュ通知が出るようになります。
creact-react-app
で作られたApp.tsx
を変えただけの簡単なコードです。
押したらプッシュ通知を出すボタンと音の有無を切り替えるボタンが表示されます。
一応完成のコード公開したので実際に試したい場合はクローンしてみてください!
https://github.com/NozomuTsuruta/simple-notification-audio終わりに
ここまで読んでいただきありがとうございました!少しでもお役に立てれば嬉しいです
また、「もっといい方法あるよ」だったり、「こういう面白い技術があるよ」などなど気軽にコメントやTwitterで絡んでくれたりすると嬉しいです!
いろいろな技術を触りながら日々精進していきます!
- 投稿日:2021-01-29T22:13:23+09:00
Next.jsのプロジェクトにTypeScriptを導入する方法
Next.jsの公式チュートリアルにそって進めます
導入手順
①プロジェクトルートにtsconfig.jsonを追加。
$ touch tsconfig.json②TypeScriptのパッケージをインストール。
# If you’re using npm $ npm install --save-dev typescript @types/react @types/node # If you’re using Yarn $ yarn add --dev typescript @types/react @types/node③ローカルの開発サーバの際立ち上げ。ここで、tsconfig.jsonの中身が記述され、next-env.d.tsが追加されます。
$ npm run dev or yarn dev④JS ファイルを TS ファイルに変換します。
$ find pages -name "_app.js" -or -name "index.js" | sed 'p;s/.js$/.tsx/' | xargs -n2 mv & \ find pages/api -name "*.js" | sed 'p;s/.js$/.ts/' | xargs -n2 mv以上です。
- 投稿日:2021-01-29T22:06:52+09:00
PuppeteerでローカルのHTMLファイルをData URI Schemeとして読み込む
Puppeteerの
page.goto()
はhttpプロトコルやfileプロトコルなどの他、Data URI scheme文字列も引数にできる。下記のようなHTMLファイルを用意しておき
test.html<html> <head> <meta charset="utf-8"/> </head> <body> Data URI schemeを表示できます </body> </html>同じディレクトリのJavaScriptからこのように読み込むと、
import fs from 'fs/promises'; import puppeteer from 'puppeteer'; const html = "./test.html" const buffer = await fs.readFile(html) const browser = await puppeteer.launch({ headless: false, }); const page = (await browser.pages())[0]; await page.goto(`data:text/html;base64,${buffer.toString("base64")}`); // ... browser.close();Chromiumで開いてくれる。
実運用ではまず使わないけど、ちょっとした確認をするときに便利だったりする。なお、日本語が含まれていると、高確率で文字化けするので、charset指定をしておくのが無難。1
ファイルをそのままChromiumで開いたら文字化けしなくても、なぜかData URI schemeだと化ける。なぜだ? ↩
- 投稿日:2021-01-29T22:06:22+09:00
【JavaScript】スマホ判定する方法
プログラミング勉強日記
2021年1月29日
JavaScriptでデバイスがスマホかどうか判定する方法について簡単にまとめる。UserAgentから判定する
UserAgentがiPhoneまたはAndroidとMobileを含む場合はスマホと判定できる。接続してきたユーザー情報を知るためにnavigatorオブジェクトのUserAgentを使用する。navigatorオブジェクトはいくつもの端末やブラウザの情報が格納されていて、プロパティとして情報を取得できる。
userAgentに含まれている情報の例Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/577.01 (KHTML, like Gecko) Version/6.8 Mobile/11A111 Safari/9788.91function isSmartPhone() { // UserAgentからのスマホ判定 if (navigator.userAgent.match(/iPhone|Android.+Mobile/)) { return true; } else { return false; } }デバイスの画面幅から判定する
window.matchMedia()関数を使うことで、デバイス画面幅を判定できる。この方法はスマホを判断するのではなく、画面幅によってUIを変更したいときに使うのが望ましい。
function isSmartPhone() { // デバイス幅が640px以下の場合にスマホと判定する if (window.matchMedia && window.matchMedia('(max-device-width: 640px)').matches) { return true; } else { return false; } }サンプルプログラム
<html> <head> <title>スマホ判定</title> </head> <body> <p>使用中の端末:</p> <p id="Terminal"></p> <script type="text/javascript"> let tarminal = document.getElementById('Terminal') let msg = "" let ut = navigator.userAgent; if(ut.indexOf('iPhone') > 0 || ut.indexOf('iPod') > 0 || ut.indexOf('Android') > 0 && ut.indexOf('Mobile') > 0){ msg = "SmartPhon"; }else if(ut.indexOf('iPad') > 0 || ut.indexOf('Android') > 0){ msg ="Tablet"; }else{ msg = "Personal Computer"; } tarminal.textContent = msg </script> </body> </html>参考文献
JavaScriptでスマホ判定する2つの方法
JavaScriptでアクセス元の端末がスマホかどうか判定する方法を現役エンジニアが解説【初心者向け】
- 投稿日:2021-01-29T21:11:39+09:00
マウスストーカーを実装する方法
サンプル
See the Pen mouse stalker 1 by pd_kosaka (@pd_kosaka) on CodePen.
解説
マウスの座標を取得して作成した要素が追従するように設定
遅延アニメーションはTweenMaxを使用カーソルを変更するパターン
See the Pen mouse stalker 2 by pd_kosaka (@pd_kosaka) on CodePen.
- 投稿日:2021-01-29T20:41:13+09:00
firebase deploy で詰まった時の対処法
簡単な自己紹介
今回が初投稿になります!現在フロントエンド を学習している就活生です。
何事もアウトプットが重要だと思うので投稿してみようと思いました!かつ自分の備忘録として記録しようと思います。今回vueCLIとfirebaseを使用してアプケーションを作成したのですが、最後の最後デプロイするところで壁にぶつかったので記録しておきます!
サイトが公開されていない
詳しいデプロイ方法は他の記事を参照してください。
$ firebase deploy
これでfirebase deployが成功した場合、最後にURLが発行されると思います。
そしてそのURLをクリックして無事公開できたと思ったらこの画面になりました、、、
原因
おそらく人によって様々かとは思いますが私の場合はdistディレクトリの位置がおかしかったからでした。
$firebase init ######## #### ######## ######## ######## ### ###### ######## ## ## ## ## ## ## ## ## ## ## ## ###### ## ######## ###### ######## ######### ###### ###### ## ## ## ## ## ## ## ## ## ## ## ## #### ## ## ######## ######## ## ## ###### ######## You're about to initialize a Firebase project in this directory: C:\Users\username\ ⬆︎ここがおかしい上記のようになっている場合うまくいきません
C:\Users\username\desktop\app
このようにしておかなければなりませんでした。
解決策
私の場合すでに作成しまっていたdistディレクトリを削除し、また新しく作り直したのちに
正しいディレクトリの位置でfirebase init を行いました。そしてもう一度firebase deploy を実行すると成功しました。
これまでターミナルなんて物を触ったこともなかったのでこんなこと当たり前だよ!と言われそうですが、
何故無知な者で。。
これからも精進していきます!
- 投稿日:2021-01-29T20:22:30+09:00
【React】Reducerでstateを直接変更してはならないことの背景
Reducerのルール
Reducerにはいくつかの決まりごとがあり、よくあるミスとして「引数として渡されたstateそのものに変更を加えてはならない」ところを、直接変更を加える処理をreducerに記述してしまうというものがあります。
ルールの背景
各reducerをまとめてexportするときにcombineReducers関数を用いますが、このcombineReducersのコード( https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts )を確認すると、終盤辺りに以下のようなコードがあります。
combineReducers.tslet hasChanged = false const nextState: StateFromReducersMapObject<typeof reducers> = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] const previousStateForKey = state[key] const nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === 'undefined') { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length return hasChanged ? nextState : stateこのコードは、受け取ったactionを各reducerに送り、reducerから返ってきたstateが更新されているかどうかを判断して、更新されていれば更新後のstateを、そうでなければ元々のstateを返すというものです。
forループでは、各reducerについてfor文内の処理が行われます。
for文内では、更新前のstateがpreviousStateForKeyに代入されます。
次に、reducerが実行され、reducerが返した更新後のstateがnextStateForKeyに代入されます。そして、次の1行でstateの更新の有無が判定されます。
hasChanged = hasChanged || nextStateForKey !== previousStateForKeyこの行では、previousStateForKeyとnextStateForKeyの比較が行われ、両者が異なっている(previousStateForKey !== nextStateForKey がTrueである)ならば、hasChangedはTrueとなり、更新後のstateが返されることになります。Falseであれば、更新前のstateがそのまま返されます。
ここで、次のようなケースを考えます。
const state = {name:"Taro",nation:"USA"}; const previousState = state; const reducer = (state) => { state.nation = "Japan"; return state; }; const newState = reducer(state);上のreducerは、stateを直接変更してしまっています。(実際のreducerではactionも引数として渡されますが、ここでは簡略化のために省いています。)
このような場合、次の真偽値はfalseとなります。これは、オブジェクトは参照渡しであるということから、newStateとpreviousStateは同じアドレスを参照しているためです。
newState !== previousState //falseこの結果、先程のcombineReducersのコードにおけるhasChangedはFalseになり、stateを更新したはずなのに更新されていないというような問題が起こるという訳です。
実は、2019年頃に次の一行が加わっており( https://github.com/reduxjs/redux/pull/3490/commits/001a1979372dbd9cf431805f439a179eb05e20be )combineReducersの変更検知の方法も少し変わったようですが、reducerでstateを直接変更しないという習慣は今後も続けた方が間違いはないと思います。
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length参考文献
・Redux combineReducers.ts( https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts )
・Udemy講座「 Modern React with Redux [2020 Update] 」( https://www.udemy.com/course/react-redux/ )
この講座は、Reactにおける様々な決まりごとの背景なども教えてくれます。
今回、reducerの決まりごとに関してここで学んだ内容について、備忘録を兼ねて共有させていただきました。
- 投稿日:2021-01-29T18:28:40+09:00
「ジャバスクリプト」関数定義
関数とは
Rubyでいうところのメソッドを、JavaScriptでは関数と呼びます。
Rubyでメソッドを定義する際はdef メソッド名 ~ endと記述しました。一方で、JavaScriptではfunction 関数名(){ 処理 }と記述することで関数を定義することができます。
function
functionを用いることで関数を定義することができます
function 関数名( ) { // 処理 };JavaScriptの戻り値
JavaScriptでは関数の戻り値をreturnを用いて明示する必要があります。
function calc(num1,num2){ return num1*num2 }; const num1 = 3 const num2 = 4 console.log(calc(num1,num2))上記のように最終的に返したい結果の部分にreturnをつけてあげてください。
関数宣言と関数式について
関数宣言については上記で紹介した
function 関数名( 引数 ){ // 関数内の処理 };特徴として下記で紹介する関数式とは違い関数宣言は先に読み込まれます。
順番に読み込まれるのではないので定義している文より先に呼び出す文があってもエラーにならず結果を返してくれます。関数宣言
一方で、関数式の場合は、function(){}という無名の関数を変数に定義または代入して関数を定義する、という記述になります。
変数 = function( 引数 ){ // 関数内の処理 };こちらは逆に順に読み込まれるので定義前に呼び出したりすると定義されていないよとエラーになること間違い無いでしょう。
関数式の種類
無名関数
無名関数は、関数名なしで関数を定義することができます。より簡潔なコードが記述できるというメリットがあります。
// 関数宣言 function hello(){ console.log('hello') }; // 関数式(無名関数) const hello = function(){ console.log('hello') };即時関数
即時関数とは、関数を定義すると同時に実行される構文です。関数を定義してから呼び出すという手間を省くことができます。
()の中にfunctionからはじまる関数定義そのものを配置することで、その関数を即実行するということができるようになります。その結果、関数を呼び出すという手間が省けます。// 無名関数 const countNum = function(num) { console.log(num) } countNum(1); // 即時関数 (function countNum(num) { console.log(num) })(1);アロー関数
アロー関数とは以下のようにfunctionの記述を省略し、その代わりに()=>という記述によって関数を定義する構文です。より短い記述で関数定義をできるというメリットがあります
このように、アロー関数を用いることで短い記述で関数を定義することができます。// 無名関数 const 変数名 = function(){ 処理 }; // アロー関数 const 変数名 = () => { 処理 };
- 投稿日:2021-01-29T18:28:40+09:00
「JavaScript」関数定義
関数とは
Rubyでいうところのメソッドを、JavaScriptでは関数と呼びます。
Rubyでメソッドを定義する際はdef メソッド名 ~ endと記述しました。一方で、JavaScriptではfunction 関数名(){ 処理 }と記述することで関数を定義することができます。
function
functionを用いることで関数を定義することができます
function 関数名( ) { // 処理 };JavaScriptの戻り値
JavaScriptでは関数の戻り値をreturnを用いて明示する必要があります。
function calc(num1,num2){ return num1*num2 }; const num1 = 3 const num2 = 4 console.log(calc(num1,num2))上記のように最終的に返したい結果の部分にreturnをつけてあげてください。
関数宣言と関数式について
関数宣言については上記で紹介した
function 関数名( 引数 ){ // 関数内の処理 };特徴として下記で紹介する関数式とは違い関数宣言は先に読み込まれます。
順番に読み込まれるのではないので定義している文より先に呼び出す文があってもエラーにならず結果を返してくれます。関数宣言
一方で、関数式の場合は、function(){}という無名の関数を変数に定義または代入して関数を定義する、という記述になります。
変数 = function( 引数 ){ // 関数内の処理 };こちらは逆に順に読み込まれるので定義前に呼び出したりすると定義されていないよとエラーになること間違い無いでしょう。
関数式の種類
無名関数
無名関数は、関数名なしで関数を定義することができます。より簡潔なコードが記述できるというメリットがあります。
// 関数宣言 function hello(){ console.log('hello') }; // 関数式(無名関数) const hello = function(){ console.log('hello') };即時関数
即時関数とは、関数を定義すると同時に実行される構文です。関数を定義してから呼び出すという手間を省くことができます。
()の中にfunctionからはじまる関数定義そのものを配置することで、その関数を即実行するということができるようになります。その結果、関数を呼び出すという手間が省けます。// 無名関数 const countNum = function(num) { console.log(num) } countNum(1); // 即時関数 (function countNum(num) { console.log(num) })(1);アロー関数
アロー関数とは以下のようにfunctionの記述を省略し、その代わりに()=>という記述によって関数を定義する構文です。より短い記述で関数定義をできるというメリットがあります
このように、アロー関数を用いることで短い記述で関数を定義することができます。// 無名関数 const 変数名 = function(){ 処理 }; // アロー関数 const 変数名 = () => { 処理 };
- 投稿日:2021-01-29T18:13:20+09:00
「JavaScript」基本1
記述を残そうと思った理由
はじめ苦手意識がありなかなか手を付けようとしなかったせいで、だいぶ内容が飛んでしまったのでさらっと復習がてらこちらに残そうと思い記述しています。
目標15分でまとめて行きたいので雑になりますJavaScriptとは何か?
JavaScript(ジャバスクリプト)とはプログラミング言語の1つです。Rubyはサーバーサイドを担う言語でしたが、JavaScriptは主にクライアントサイドにおいて力を発揮します。すなわち、「ブラウザ上でのアプリケーションの使いやすさ」や「リクエストの送り方の工夫」をJavaScriptは担っています。
JavaScriptによって、より使い勝手の良いアプリケーションの作成ができます。Ruby on RailsのWebアプリケーションにJavaScriptを適用する方法はこのあとのLESSONで学びます。本LESSONではJavaScriptのみを学び、より使い勝手の良いアプリケーションを作成するための基礎を身に着けましょう。
JavaScriptは多くの開発現場でも用いられているプログラミング言語です。
JavaScriptは画面を更新せずとも、サーバーと通信ができるためページ遷移なしで、画面の表示を切り替えられる。
動的動きをつけることができる。つまり
かっこよくできるし、便利にできるし、いろいろなことができるよって解釈
特徴は?
デベロッパーツール
コンソールでデバック
コマンドは⌘ + option + C基礎構文
JavaScriptでは変数定義の様式は、var、const、letと3つ存在します。
var
古い書き方
もう使わないconst
再代入ができない書き方
let
再代入ができる書き方
再定義は不可要は
let は再代入する予定のある変数を定義する際に使用する
const は再定義する予定のない変数を定義する際に使用するテンプレートリテラル
テンプレートリテラルは、JavaScriptの構文
ダブルクォートやシングルクォートの代わりにバッククォートで囲むことで、文字列内に挿入することができる。
要は
文字列の中に変数を埋め込むことができる
改行を入れることができる
HTMLの要素を作成することができるconst name = "Tom" const age = 25 const introduction = `私の名前は${name}、${age}歳です` console.log(introduction) // => 私の名前はTom、25歳です と出力される条件分岐
条件を満たしているかどうかで実行内容を分岐する処理
if (条件式1) { // 条件式1がtrueのときの処理 } else if (条件式2) { // 条件式1がfalseで条件式2がtrueのときの処理 } else { // 条件式1も条件式2もfalseのときの処理 }for文
繰り返し処理を行う
for ([①初期化式]; [②条件式]; [③加算式]) { // 繰り返す処理の内容 }forEach関数
配列に対する繰り返し処理
配列.forEach( function(value){ // 処理の記述 })
- 投稿日:2021-01-29T17:26:10+09:00
javascriptでシューティングゲームみたいなものを作る②
こんにちはrihitoです。
javascriptでシューティングゲームみたいなものを作る第二回目です。動機
qiita初投稿で何を書こうか考えていたら、javascriptでゲームを作ろう!
と思ったので適当に書きます。対象読者
・javascriptをある程度できる人
・ゲームを作りたい人
・暇な人
・とりあえず何かしたい人今回は、playerの攻撃などを実装していきたいと思います。
playerの攻撃を実装する
スペースキーが押されたらplyerにレーザーを描画する
まずレーザーを描く
main.js_8行目~//レーザーの座標 var lx =[0]; //レーザーをたくさん描けるようにするためリストにする var ly = [0];main.js_14行目~//playerを描く関数 function player_draw(){ ctx.beginPath() ctx.rect(px,py,10,10) ctx.fillStyle = "#00ff00" ctx.fill() ctx.closePath() } //追加******************** function l_draw(){ ctx.beginPath() ctx.rect(lx[0],ly[0],10,3) ctx.fillStyle = "#ff0000" ctx.fill() ctx.closePath() } //追加ここまで************main.js_46行目~function draw(){ ctx.clearRect(0/*開始地点*/,0,canvas.width/*終了地点*/,canvas.height) //canvasをいったんクリアする player_draw() //追加 l_draw() //playerを動かす px += p_dx py += p_dy }ly[0] は リストlyの0番目ということです。
動かしてみる
main.js_46行目~function draw(){ ctx.clearRect(0/*開始地点*/,0,canvas.width/*終了地点*/,canvas.height) //canvasをいったんクリアする player_draw() //追加 l_draw() for(var i = 0;i < lx.length/*リストの長さ*/;i++){ //リストを読み込む lx[i] += 1 //今読み込んでいるレーザーを動かす } //playerを動かす px += p_dx py += p_dy }なぜforを使っているのかというとこれは、本当はこう書きたいからです。
lx[0] += 1 //lxの0番目に1をたす
lx[1] += 1 //lxの1番目に1をたす
lx[2] += 1 //lxの2番目に1をたす
lx[3] += 1 //lxの3番目に1をたす
456789...スペースキーが押されたときに発射する
main.js_22行目~function l_draw(){ for(var i = 0;i < lx.length;i++){ //追加 ctx.beginPath() ctx.rect(lx[i],ly[i],5,2) ctx.fillStyle = "#ff0000" ctx.fill() ctx.closePath() }//追加 }main.js_32行目~//キーが押されたときに実行される document.onkeydown = function(e){ if(e.key == "ArrowUp"){ //↑ p_dx = 0 p_dy = -1 } if(e.key == "ArrowDown"){//↓ p_dx = 0 p_dy = 1 } //追加*************************** if(e.key == " "){ lx.push(px) //レーザー発射開始位置(playerの位置)をリストに追加 ly.push(py) } //追加ここまで******************* }enemyを描く
playerの攻撃とさほど変わりはありません。
main.js_13行目~//enemyの座標 var ex = [0]; var ey = [0];main.js_35行目~function e_draw(){ for(var i = 0;i < lx.length;i++){ ctx.beginPath() ctx.rect(ex[i],ey[i],7,7) ctx.fillStyle="#ff00ff" ctx.fill() ctx.closePath() } }main.js_71行目~for(var i = 0;i < lx.length/*リストの長さ*/;i++){ //リストを読み込む lx[i] -= 1 //今読み込んでいるレーザーを動かす }今回のコード
index.html<!--前回と同じなので省略-->
main.jsvar canvas = document.getElementById("main");//canvasを読み込む var ctx = canvas.getContext("2d"); var px = 10 //player x座標 var py = 120 //player y座標 var p_dx = 0 //player xの速さ var p_dy = 0 //player yの速さ //レーザーの座標 var lx =[0]; //レーザーをたくさん描けるようにするためリストにする var ly = [0]; //enemyの座標 var ex = [0]; var ey = [0]; //playerを描く関数 function player_draw(){ ctx.beginPath() ctx.rect(px,py,10,10) ctx.fillStyle = "#00ff00" ctx.fill() ctx.closePath() } function l_draw(){ for(var i = 0;i < lx.length;i++){ ctx.beginPath() ctx.rect(lx[i],ly[i],5,2) ctx.fillStyle = "#ff0000" ctx.fill() ctx.closePath() } } function e_draw(){ for(var i = 0;i < lx.length;i++){ ctx.beginPath() ctx.rect(ex[i],ey[i],7,7) ctx.fillStyle="#ff00ff" ctx.fill() ctx.closePath() } } //キーが押されたときに実行される document.onkeydown = function(e){ if(e.key == "ArrowUp"){ //↑ p_dx = 0 p_dy = -1 } if(e.key == "ArrowDown"){//↓ p_dx = 0 p_dy = 1 } if(e.key == " "){ lx.push(px) //レーザー発射開始位置(playerの位置)をリストに追加 ly.push(py) } } //キーが離されたときに実行される document.onkeyup = function(e){ p_dx = 0 //止める p_dy = 0 } function draw(){ ctx.clearRect(0/*開始地点*/,0,canvas.width/*終了地点*/,canvas.height) //canvasをいったんクリアする player_draw() l_draw() e_draw() for(var i = 0;i < lx.length/*リストの長さ*/;i++){ //リストを読み込む lx[i] += 1 //今読み込んでいるレーザーを動かす } e_draw() for(var i = 0;i < ex.length/*リストの長さ*/;i++){ //リストを読み込む ex[i] -= 1 //今読み込んでいるenemyを動かす } //playerを動かす px += p_dx py += p_dy } setInterval(draw,10) //10ミリ秒単位で実行 draw();
- 投稿日:2021-01-29T16:47:16+09:00
Puppeteerで使わなれないタブがあるのが気になる
Puppeteerで、よく以下のような書き方を見ます。
const browser = await puppeteer.launch({ headless: false, }); const page = await browser.newPage(); await page.goto("https://www.google.com/");このように書くと、以下のように新しいタブでURLが開かれます。
このとき、利用していない「about:blank」なタブがずっとあるのが気になっていました。(害はないんですが。)
Chromiumって起動時にタブを1個開いているんですよね。Puppeteerのドキュメントを見ると、browser.pages()でページ一覧が取れるので、
const browser = await puppeteer.launch({ headless: false, }); const page = (await browser.pages())[0]; await page.goto("https://www.google.com/");こうすると、もとからあるタブを使ってくれました!
- 投稿日:2021-01-29T16:44:48+09:00
React + react-use: useKeyフックでキーボードイベントを扱う
react-use
は、便利なフックを詰め合わせたライブラリです。本稿はその中から、キーボードイベントが簡単に扱えるuseKey
をご紹介します。作例はCodeSandboxに公開しました。Create React Appでカウンターアプリケーションをつくる
サンプルにするのは、よくとり上げられるカウンターです。React公式サイトの「ステートフックの利用法」でも、
useState
フックの説明に使われています。Reactアプリケーションのひな型は、モジュール分けできるCreate Reac Appでつくることにしましょう(「Create React App 入門 01: 3×3マスのゲーム盤をつくる」01「Reactアプリケーションのひな形をつくる」参照)。
npx create-react-app react-usekey
ルートコンポーネントのモジュール
src/App.js
は、つぎのコード001のように書き改めます。useState
でカウンター数値の状態変数(count
)をひとつ定め、減算(decrement
)と加算(increment
)のハンドラを加えました。CounterDisplay
は、このあと新たにつくるカウンター表示のコンポーネントです。コード001■useStateとプロパティを用いたルートモジュール
src/App.jsimport React, { useState } from 'react'; import CounterDisplay from './CounterDisplay'; const initialCount = 0; function App() { const [count, setCount] = useState(initialCount); const decrement = () => setCount((count) => --count); const increment = () => setCount((count) => ++count); return ( <div className="App"> <CounterDisplay counter={{ count, decrement, increment }} /> </div> ); } export default App;カウンターを表示するコンポーネントのモジュール
src/CounterDisplay.js
は、つぎのコード002のように定めました。コンポーネントが受け取ったプロパティ(counter
)から、カウンター数値(count
)と減算(decrement
)・加算(increment
)のハンドラを取り出して、JSXのテキストとボタンに与えましょう。コード002■カウンター表示のモジュール
src/CounterDisplay.jsimport React from "react"; const CounterDisplay = ({ counter }) => { return ( <div> <button onClick={counter.decrement}>-</button> <span>{counter.count}</span> <button onClick={counter.increment}>+</button> </div> ); } export default CounterDisplay;これで、数値をボタンで増減できるカウンターのでき上がりです(図001)。まだ、キーボードイベントには対応していません。
図001■でき上がったカウンター
react-useのuseKeyフックを使う
では、いよいよ
useKey
フックを使います。まずは、アプリケーションへのreact-use
のインストールです。npm install react-use
そうしたら、キーボードイベントを扱いたいコンポーネントに、
useKey
をimport
してください。上下の矢印キーで、カウンターを増減したいと思います。
useKey
の使い方は、つぎのように気が抜けるほど簡単です。第1引数にキーを示すKeyboardEvent.key
プロパティの値、第2引数にイベントハンドラを渡します。JSXのどの要素にイベントハンドラを加えるか、などと悩まなくて済むのです。src/App.jsimport { useKey } from 'react-use'; function App() { useKey('ArrowDown', decrement); useKey('ArrowUp', increment); }ここまでのルートモジュール
src/App.js
の記述全体を、つぎのコード003にまとめました。コード003■上下矢印キーでカウンターを増減させるルートモジュール
src/App.jsimport React, { useState } from 'react'; import {useKey} from 'react-use'; import CounterDisplay from './CounterDisplay'; const initialCount = 0; function App() { const [count, setCount] = useState(initialCount); const decrement = () => setCount((count) => --count); const increment = () => setCount((count) => ++count); useKey('ArrowDown', decrement); useKey('ArrowUp', increment); return ( <div className="App"> <CounterDisplay counter={{ count, decrement, increment }} /> </div> ); } export default App;[control]/[Ctrl] + [esc]キーでカウンターをリセットする
キーボードイベントの処理をもうひとつ加えます。[control]/[Ctrl] + [esc]キーを押したら、カウンターの数値を0にリセットしましょう。[esc]キーの
KeyboardEvent.key
プロパティの値はEscape
です。[control]/[Ctrl]キーを押しているかどうかは、KeyboardEvent.ctrlKey
で調べられます。
useKey
のつぎの構文で、第1引数はキー判定する関数です。キーイベントを受け取るので、ハンドラを呼び出すキーかどうか論理値で返します。第3引数のevent
プロパティに定めるのは、keydown
/keypress
/keyup
のいずれかのキーボードイベントです。const キー判定関数 = (event) => 判定した論理値; useKey(キー判定関数, イベントハンドラ, {event: イベント});キー判定関数では、つぎのように[control]/[Ctrl] + [esc]キーをイベント(
event
)のプロパティから調べます。キーボードイベントはkeyup
としました。ルートモジュールの記述は、以下のコード004にまとめたとおりです。作例はCodeSandboxに公開しましたので、動きやモジュールごとのコードはこちらでお確かめください。src/App.jsfunction App() { const predicate = (event) => event.ctrlKey && event.key === 'Escape'; const escKeyUpHandler = () => setCount(0); useKey(predicate, escKeyUpHandler, {event: 'keyup'}); }コード004■[control]/[Ctrl] + [esc]キーでカウンターをリセットするルートモジュール
src/App.jsimport React, { useState } from 'react'; import {useKey} from 'react-use'; import CounterDisplay from './CounterDisplay'; const initialCount = 0; function App() { const [count, setCount] = useState(initialCount); const decrement = () => setCount((count) => --count); const increment = () => setCount((count) => ++count); useKey('ArrowDown', decrement); useKey('ArrowUp', increment); const predicate = (event) => event.ctrlKey && event.key === 'Escape'; const escKeyUpHandler = () => setCount(0); useKey(predicate, escKeyUpHandler, {event: 'keyup'}); return ( <div className="App"> <CounterDisplay counter={{ count, decrement, increment }} /> </div> ); } export default App;
- 投稿日:2021-01-29T16:32:07+09:00
Laravelでほんの少しハイレベルな検索機能を作ってみた。(初心者向け)
こんにちは、しろうです。
現在、Laravelで小説サイトを作成しています。
そんな中で、(自分的には)ちょっとだけハイレベルな検索機能を作ってみたので、忘れないようにメモとして残しておきます。
検索機能を実装したい人はぜひ、参考にしてください。
もしミスとかあればコメントして頂けると大変有り難いですm(_ _)m
前置き
・クラス名とかひどいのあるかと思いますが、気にしないで頂けると幸いです。
・Formファサードを使ったり、素のformを使ったり混在していますが、許してくださいm(_ _)m
・cssは貼り付けていないので、皆さん側で好きなように変更してください。今回作る検索機能
・複数条件での絞り込み検索ができる。(例:キーワードとジャンルで絞り込む。)
・現在の検索条件がタグとして表示される。
・タグを削除すれば、検索条件から削除される。検索機能のイメージ動画
ちなみにイメージ動作は下記の通りです。
下記のように、他のページからジャンルとかをクリックしたら、そのジャンル名で検索してくれる機能もつけてます。
さて、じゃあどんどん作っていきます。
とりあえず検索機能を作る
viewファイルの作成(検索項目の部分)
とりあえず検索項目を作成していきます。
今回は「キーワード検索」と「ジャンル検索」のみを作成しています。
また、CSSは貼り付けないので、皆さんの方で好きなように変更よろしくお願いしますm(_ _)msearch.blade.php{{ Form::open(['route'=>'search','method'=>'GET','id'=>'side_search_form','autocomplete'=>"off"]) }} <div class="search_keyword"> <div class="search"> <input name="keyword" value="{{request('keyword')}}" type="search" class="searchTerm" placeholder="キーワード検索"> <button type="submit" class="searchButton" form="side_search_form"> <i class="fa fa-search fa-xs"></i> </button> </div> </div> <div class="search_genre search_side_item_border"> <h4>ジャンル</h4> <div class="ripples_radio"> {{ Form::radio('genre','', isset(request()->genre) ? false :true, ['id'=>'search_genre_none']) }} {{ Form::label('search_genre_none', '指定なし', []) }} </div> @foreach ($genres as $index => $genre) <div class="ripples_radio"> {{ Form::radio('genre',$genre, $genre == request()->genre ? true :false, ['id'=>'search_genre'.$index]) }} {{ Form::label('search_genre'.$index, $genre, []) }} </div> @endforeach </div> {{ Form::close() }}解説していきます。
・
'autocomplete'=>"off"
・・・これをつけると検索履歴が表示されなくなります。お好みでどうぞ。
・value="{{request('keyword')}}"
・・・このようにするとvalue値が「keywordというパラメーター(クエリ文字列)の値」になります。
・requestメソッド
・・・リクエストデータを取得できるやつです。・
@foreach ($genres as $index => $genre)
・・・この$genres
はコントローラーから検索できるジャンル名を返しています。単純にジャンル名を1つずつ処理しているだけです。下記少しわかりにくいかと思います。
{{ Form::radio('genre',$genre, $genre == request()->genre ? true :false, ['id'=>'search_genre'.$index]) }}
Form::radio
は第三引数がtrueならchecked
をつけるので、ジャンル名とパラメーターのジャンル名(現在検索しているジャンル名)が一致するなら、checked
をつけるようにしています。例えば、
http://127.0.0.1:8000/search?genre=恋愛(現世)
とかなら、request()->genre
によって、恋愛(現世)
という文字列を取得できます。なので、
$genre == request()->genre
とすることで、現在検索しているジャンルボタンにのみchecked
をつけることができます。
指定なし
というラジオボタンもほしいので、下記コードで作成しています。{{ Form::radio('genre','', isset(request()->genre) ? false :true, ['id'=>'search_genre_none'])}} {{ Form::label('search_genre_none', '指定なし', []) }}
value属性
を''(空文字)
にしておけば、コントローラー側の処理でいい感じにできます。(解説は後ほど。)web.phpにルーティングを追加
とりあえず、なんでもいいのでルーティングを追加します。
web.phpRoute::get('search', 'UsersController@search')->name('search');ジャンル用のファイルを作成
今回は
config/getValue/radio.php
にジャンル用の配列を作成しました。config/getValue/radio.php<?php return [ 'genre' =>[ '1' =>'恋愛(異世界)', '2'=>'恋愛(現世)', '3'=>'ラブコメ', '4'=>'ホラー', '5'=>'推理(ミステリー小説)', '6'=>'異世界ファンタジー', '7'=>'現代ファンタジー', '8'=>'コメディ', '9'=>'SF', '10'=>'詩・エッセイ・童話', '11'=>'歴史・戦国', '12'=>'その他', ], ];このようにすることで
config('getValue.radio.genre')
とすれば、ジャンル名の配列を取得することができます。コントローラー内でこのジャンル名の配列を取得します。
コントローラーに処理を追加
次にコントローラー側に検索処理を記述していきます。
UsersController.phppublic function search(Request $request) { //SQL文を書くためにqueryメソッドを使う。 $query = Novel::query(); //ジャンル名を取得。(viewファイルに返すだけ。) $genres = config('getValue.radio.genre'); //keywordがあるかどうか。 if ($request->filled('keyword')) { //検索キーワードとタイトルが一致するレコードを絞り込む $query->where('title', 'like', '%'.$request->get('keyword').'%'); } //genreがあるかどうか。 if ($request->filled('genre')) { //検索ジャンルとジャンル名が一致するレコードのidをgenresテーブルから取得 $genre_id = Genre::where('name', $request->genre)->first()->id; //genre_idが一致するレコードをnovelsテーブルから絞り込む $query->where('genre_id', $genre_id); } //条件に一致するレコードを作成日で降順に並び替えて取得 $novels = $query->latest('novels.created_at')->paginate(50); //viewファイルは好きなファイルにしてください。 return view("search", compact('novels', 'genres')); }たぶん、ほとんど読めばわかるんじゃないかと思います。
$request->filled('genre')
を使えば、リクエストに値が存在して、かつ、空でない場合
にtrueを返してくれます。似たものに
$request->has('genre')
というのがありますが、これだと空であってもtrueになります。それだと、下記のような
指定なし(value値が空文字列)
の場合でも実行されてしまうので、今回はfilledメソッド
を使用しています。{{ Form::radio('genre','', isset(request()->genre) ? false :true, ['id'=>'search_genre_none'])}} {{ Form::label('search_genre_none', '指定なし', []) }}ちなみに
latest()
を使えば、降順に並び替えてくれます。(下記の通り)$query->latest('novels.created_at')->paginate(50);あとは、好きなようにviewファイル側でデザインしてあげれOK。
試しに
search.blade.php
とかで{{dd($novels)}}
とかで中身を確認してみてください。現在の検索条件を表示する機能の作成
なんて説明すればいいかわかりませんが、ここからは上記の画像のやつを作っていきます。笑
方法としては、そこまで難しくありませんので、ご安心ください!!!
まずはview側に処理を追加
とりあえず、viewファイルに処理を追加していきます。
下記のようにすれば現在の検索条件を表示することができます。
search.blade.php<div class="search_condition"> <ul> @if(!empty(request()->keyword)) <li class="search_condition_item"> {{request()->keyword }}<a href="keyword" class="search_condition_a"><i class="fas fa-times search_condition_delete"></i></a> </li> @endif @if(!empty(request()->genre)) <li class="search_condition_item"> {{request()->genre }}<a href="genre" class="search_condition_a"><i class="fas fa-times search_condition_delete"></i></a> </li> @endif </ul> </div>見て大体わかると思いますので、ざっくり説明していきます。
まず
empty()
は引数が空ならtrueを返すので、!(エクスクラメーション)
をつけて、中身が空ではない時
にif文内のhtmlが表示されるようにします。そして
if文の条件
を「!empty(request()->keyword)
」みたいな感じにすることで、現在keywordで検索をしているのかどうか、がわかります。(このkeyword
というのはinputのname属性
のことです。)例えば、
http://127.0.0.1:8000/search?keyword=&genre=
みたいな感じで、keyword
が空になっているなら、処理は実行されません。逆に
http://127.0.0.1:8000/search?keyword=人生楽しいね&genre=
とかだと、「人生楽しいね」というキーワードで検索されていることになるので、if文内の処理が実行されます。そして、if文内の処理は下記のようにします。
<li class="search_condition_item"> {{request()->keyword }}<a href="keyword" class="search_condition_a"><i class="fas fa-times search_condition_delete"></i></a> </li>まず
{{request()->keyword }}
とすることで「人生楽しいね」が表示されます。
でもって、aタグのhref属性
にkeyword
と記述して、JavaScriptでこいつを操作していきます。JavaScriptを記述
ここからはJavaScript(今回はjQuery)の出番です。
なんでもいいので、JavaScritのファイルを作って、下記のように記述してください。
search.js$(function () { $('.search_condition_a').each(function () { //href属性を取得 attr = $(this).attr('href'); //初期化 var url = new URL(window.location); //パラメーター削除 url.searchParams.delete(attr); if (url.search) { $(this).attr('href', 'search' +url.search); } else { $(this).attr('href', 'search'); } }); });まず、
$('.search_condition_a').each(function () {}
とすることで、search_condition_aクラス
の要素(aタグの部分)をループさせています。
attr = $(this).attr('href');
とすることで、ループされたaタグのhref属性を取得しています。例えば、下記の場合は
keyword
という文字列が取得できます。<a href="keyword" class="search_condition_a">次にURLオブジェクトを作成し、
searchParamsメソッド
とdeleteメソッド
を使用して、先ほど取得した、href属性の初期値
と一致するパラメーターを削除します。//初期化(URLオブジェクトの作成) var url = new URL(window.location); //パラメーター削除 url.searchParams.delete(attr);もう少し詳しく解説していきます。
URLオブジェクトとは、URLを作成したり、編集したりするときに使えるメソッドが沢山入っている、JavaScriptが用意してくれている便利なやつです。()
参考:URL()
そして、引数には
window.location
として、現在開いているURLを与えます。次に
searchParams
で、URLのパラメーター(URLの?以降の部分)を取得、deleteメソッドで引数に与えたものと一致するパラメーターを削除します。例えば、
attr
にkeyword
という文字列が入っている場合は、url.searchParams.delete(attr);
によって、・
http://127.0.0.1:8000/search?keyword=人生楽しいね&genre=恋愛(異世界)
からkeywordの部分が削除されるので、
・http://127.0.0.1:8000/search?genre=恋愛(異世界)
こんな感じになります。最後にパラメーターがあるかないかで条件分岐させています。
こうしないと、他のページから単一条件で検索ページに飛んだときに、1つのパラメーターを削除すると、全てのパラメーターがなくなり、期待通りの動作をしてくれなかったからです。(もっと良い方法もあるかもです...)if (url.search) { $(this).attr('href', 'search' +url.search); } else { $(this).attr('href', 'search'); }
url.search
とすることで、URLのパラメーターを取得できます。あとは
attr
メソッドを使って、href属性
の値を変更してあげればOK。これでタグの部分は完成です。
別ページから条件検索をしたい時
別ページから検索したい時は下記のように、直接href属性にパラメーターを仕込んでおけばOK。
test.blade.php<a href="{{route('search')}}?genre={{$novel->genre->name}}">{{$novel->genre->name}}</a>並び替え機能も実装
ついでに並び替え機能の紹介もしておきます。(「新着順」と「更新順」だけ)
viewファイルに追加
下記のような感じで実装しました。
search.blade.php<div class="search_sort"> <button class="sort_btn {{strpos(request()->fullUrl(), 'new') !== false || strpos(request()->fullUrl(), 'sort') === false ? 'sort_active' : ''}}" name="sort" value="new" form="side_search_form" type="submit"> <span class="spot"></span>新着順 </button> <button class="sort_btn {{strpos(request()->fullUrl(), 'update') !== false ? 'sort_active' : ''}}" name="sort" value="update" form="side_search_form" type="submit"> <span class="spot"></span>更新順 </button> </div>順に解説していきます。
まず
formタグ
の外側でボタンを作る時はform属性
でformタグのid属性
を指定します。下記の2つの部分は現在のURLによって
sort_activeクラス
を付与するかどうか三項演算子を利用して、決定しています。{{strpos(request()->fullUrl(), 'new') !== false || strpos(request()->fullUrl(), 'sort') === false ? 'sort_active' : ''}} //省略 {{strpos(request()->fullUrl(), 'update') !== false ? 'sort_active' : ''}}
strpos()
は文字列の中に指定した文字列があるかどうかを判定するメソッドです。
request()->fullUrl()
で現在のURLが取得できます。コントローラーに処理を追加
最後に下記のように処理を追加・変更します。
SearchController.phppublic function search(Request $request){ //省略 if ($request->filled('sort') && $request->sort === 'new') { $query->latest('novels.created_at'); }elseif ($request->filled('sort') && $request->sort === 'update') { $query->latest('novels.updated_at'); } else { $query->latest('novels.created_at'); } //こっちのlatestは消す。 $novels = $query->paginate(50); return view("search", compact('novels', 'genres')); }これで並び替え機能も完成です。
簡単簡単。
最後に
これで一応、イメージ動画みたいな感じの検索機能が実装できたかと思います。
昔から検索機能を作るのは苦手なので、少し苦労しました...なんか条件分岐が多いですし疲れますね。(コードのせいかも。)
駆け足だったので、わかりにくい部分があるかも・・・その時はコメントしてくださいませm(_ _)m
たぶん、もう少し仕様変更とかしますが、ひとまずこれにて終了です!お疲れ様でした。
- 投稿日:2021-01-29T16:30:22+09:00
ES2020から追加された機能
ES2020から追加された機能について
今回は、javascriptの最新バージョンES2020について、まとめました。
- Optional Chaining
- dynamic import
- Promise.allSettled()
- Nullish coalescing Operator
- for in文
- BigInt
- ウェブブラウザの対応状況
nullかundefinedになりうる値へのアクセスが簡単になるOptional Chaining
Optional Chainingとは、構文を用いてnullやundefinedになりうる値へ安全にアクセスできる仕組みです。
構文は、
A?.B Aがnullかundefinedでないとき、Bを返す
利用シーンは、nullやundefinedになるかもしれない値にアクセスしたいときなどです。
例えば、APIからユーザーの住所を取得して出力するプログラムを考えてみましょう。サーバーからは次のようなオブジェクトが返ってくる想定です。
▼ APIから返ってくる想定のオブジェクト
const area = { name: "東京", address: { city: "足立区" } };上記のオブジェクトから cityを出力したいのですが、 データによってはaddressが存在しないかもしれません。従来は、addressの存在を考慮に入れて次のようなコードを記述する必要がありました。
▼ 従来のコード
const city = area.address && area.address.city; console.log(city); // 結果は”足立区”Optional Chainingを使うと、次のようにコードが記述できます。?が Optional Chaining用の演算子です。
▼ Optional Chainingを使った新コード
const city = area.address?.city; console.log(city); // addressがない場合はundefinedになる(エラーにならない)。▼ Optional Chainingの実行結果
cityがなくてもエラーにはならない。
?.構文は何個でも使用できるので、areaもnullになりえるのであれば次のように記述できます。
▼ ?.構文を複数使う例
const city = area?.address?.city; // 結果: areaもaddressもある場合は「足立区」が出力される。 // それ以外はundefinedになる(エラーにならない)。DOM要素へquerySelector()を用いてアクセスする場合にも便利に使えます。
▼ HTML
<p>テスト</p>▼ pタグ内のテキストを出力する例
const paragraphText = document.querySelector("p")?.innerHTML; console.log(paragraphText); // 結果: p要素がある場合は、p要素内のテキストを参照して「こんにちは」が出力される。 // それ以外はundefinedになる(エラーにならない)。dynamic import(動的読み込み)
従来のimport()では、モジュールは即座に読み込まれる仕組みでした。
ですが、dynamic importでは、任意のタイミングでモジュールを読み込むことができることができるようになリました。
例えば、ページの初期表示に必要なJavaScriptだけを読み込んでおき、コンテンツが展開する度に必要なモジュールを遅延ロードすれば、ページの初期表示時の処理負荷が軽減できます。▼ import したいクラス
export class Sub { subMethod() { Console.log(“testログ”) } }尚、動作確認はGoogle Chrome、Safari、Firefox、Edgeでのみできます。
▼ 従来のimport
import {Sub} from './sub.js' const sub = new Sub(); sub.subMethod();▼ 実行結果
▼ dynamic importの場合
import('./sub.js').then(module => { // 動的に読み込まれたSubクラス const sub = new module.Sub(); sub.subMethod(); });resolve()時の引数にモジュールを受け取るため、new module.Sub()とすればsub.js内のSubクラスにアクセスできます。
▼ 動的に読み込まれたSubクラスをコントロールする
setTimeout(() => { import('./sub.js').then(module => { // 読み込み完了後にSubを使用する const sub = new module.Sub(); sub.subMethod(); }); }, 3000);▼ 3秒後にコンソールするようにセットした結果
Promiseの複数処理に便利なPromise.allSettled()
Promise.allSettled()は、複数のPromiseの処理を扱うためのメソッドです。複数のPromiseの1つがrejectされても処理を続行できます。
複数のPromiseを、成功・失敗によらずすべて実行したいとき利用できる。
従来、Promiseの複数同時処理といえば、ES2015で導入されたPromise.all()というメソッドがあります。Promise.all()は複数のPromiseの処理を実行し、全ての処理が成功した場合にはじめて処理が終了するものです。もし、1つでもrejectされるものがあればその時点で処理を終了します。▼ Promise.all()で全ての処理が成功した場合
const promiseList = [ Promise.resolve("成功1"), Promise.resolve("成功2"), Promise.resolve("成功3"), Promise.resolve("成功4") ]; Promise.all(promiseList).then( resolve => console.log(`resolve: ${resolve}`), reject => console.log(`reject: ${reject}`) );▼ 実行結果
Promiseの処理が成功のみだと、全ての処理が実行されている次に、途中どれか一つでも、処理が失敗した場合の想定で挙動を見てみる。
期待するコンソールは、成功1、成功2、失敗3、成功4▼ Promise.all()でrejectされるものがあった場合
const promiseList = [ Promise.resolve("成功1"), Promise.resolve("成功2"), Promise.reject("失敗3"), Promise.resolve("成功4") ]; Promise.all(promiseList).then( resolve => console.log('resolve: ${resolve}'),z );▼ 実行結果
Promise.allSettled()では、rejectされるものがあってもすべての処理が実行されます。したがって、複数の配列のどれかが失敗したとしても処理をすべて実行したい場合に便利です。
▼ Promise.allSettled()でrejectされるものがあった場合
const promiseList = [ Promise.resolve("成功1"), Promise.resolve("成功2"), Promise.reject("失敗3"), Promise.resolve("成功4") ]; Promise.allSettled(promiseList).then( resolveList => { console.log("resolve"); for (const resolve of resolveList) { console.log(resolve); } }, reject => { console.log("reject"); console.log(reject); } );▼ 実行結果

Reject、失敗の処理が含まれていますが、すべての処理がされています。nullかundefinedのときだけ値を返せるNullish coalescing Operator
Nullish coalescing Operatorは、「A ?? B」という形で用い、AがnullかundefinedのときだけBを返します。「coalescing」は聞き慣れない単語かもしれませんが、「合体」という意味を持ちます。0、falseなどにfalseに評価されうる値をワンライナーで正しく扱いたい時に使用します。
例えば、次のようなオブジェクトから、foo値を取得して出力する関数を考えた時、もしfooが未設定(nullまたはundefined)ならば、「値なし」という文字列を出力する。▼ 対象にするオブジェクト
const object1 = { foo: 100 }; const object2 = { foo: 200 }; const object3 = { foo: null }; const object4 = { foo: false };||(論理OR)演算子を使って、次のようなgetFoo関数を定義するのはNGです。object2、object3が引数として渡された場合にobject.fooがfalseと評価されて「値なし」が出力されるからです。
▼ fooの取得関数のダメな例
function getFoo(object) { return object.foo || "値なし"; } console.log(getFoo(object1)); console.log(getFoo(object2)); console.log(getFoo(object3)); console.log(getFoo(object4));▼ 実行結果
Nullish coalescing Operatorを使えば、object.fooがnullかundefinedの場合のみ「値なし」を返すので、目的の処理をしてくれることになります。
▼ Nullish coalescing Operatorを使った処理
function getFoo(object) { return object.foo ?? "値なし"; } console.log(getFoo(object1)); console.log(getFoo(object2)); console.log(getFoo(object3)); console.log(getFoo(object4));▼ 実行結果
2^53以上の整数を扱えるBigInt
BigInt は組み込みオブジェクトで、 Number プリミティブで表現できる最大の数、 Number.MAX_SAFE_INTEGER (2^53)よりも大きな数値を信頼できるものとして表現する方法を提供します。 BigInt は任意に巨大な整数に使用することができます。
Number.MAX_SAFE_INTEGER (2^53)以上の値を計算しようとすると、計算結果に誤差が生じます。
その誤差をなくすために使うのが、BigIntです。▼ 計算結果に誤差が生じる例
console.log(9007199254740991 + 2)期待する値は9007199254740993です。
▼ 実行結果
BitIntを使うと、2^53以上の整数も扱えるようになります。BigIntを扱うには、BigInt(数値)とするか、数値nという記述をします。BigIntはBigInt同士でしか計算ができないので、Numberとあわせて使う場合はNumberもBigIntに変換するようにする。
▼ BigIntの計算例
console.log(BigInt(Number.MAX_SAFE_INTEGER) + 2n); console.log(9007199254740991n + 2n)▼ 実行結果

今回はしっかりと期待する値が、出ました。
また、出力された9007199254740993nは数学世界での整数9007199254740993を指します。気をつける点として、BigInt はBigDecimal ではないため、演算結果は 0 の方向に丸められます。別の言い方をすれば、小数を返すことはありません。
▼ 小数点が返されない例
const expected = 4n / 2n; const rounded = 5n / 2n; console.log(expected); console.log(rounded)▼ 実行結果
本来であれば5/2は、2.5ですが、2が返されています。BigIntを使用する際は、このようなことに気をつけること。
for inの順序固定
『for...in文』は『for文』と同じような感じで繰り返し処理を実行できる構文です。『for』文は配列の各要素に対しての繰り返し処理が主な使い方でしたね。それに対して『for...in文』はオブジェクトの各プロパティに対しての繰り返し処理となります。
また、for in はインデックスの順序通りに処理を行うことが保証されていませんでした。
大抵の処理は、インデックス順で処理を行うことが多く、for inの使用は控えられてきました。var array = { "hoge": 0, "hoge1": 1, "hoge2": 2 }; for(var elem in array){ console.log(elem); }従来のfor in分では、この処理の順番が保証されないとすると、これが『0』、『2』、『1』といった順番で出力がされる可能性があったと言うことです。
保証されたと言うことなので、従来のfor in文が処理の順番が、ランダムなのではなく順番がずれる可能性があるよというような状況だってので、実際に、従来のfor in文で順番のズレが生じるのかを、実際に検証することができませんでした。ウェブブラウザの対応状況
本記事で紹介したES2020の仕様は、次のブラウザで対応しています。2020年2月時点の現行ブラウザで対応しているものには◯、対応していないものは対応開始バージョンを示しています。
機能 Chrome Firefox Safari Edge Optional Chaining v80 v74 TP 91 v81 dynamic import ◯ ◯ ◯ ◯ Promise.allSettled() ◯ ◯ ◯ ◯ Nullish coalescing Operator v80 ◯ TP 89 v81 for in文 不明 不明 不明 不明 BigInt ◯ ◯ × ◯ ※ SafariのTPはTechnology Previewを示します
ES2020の各機能に対応していないブラウザ、たとえばSafariやEdgeの現行バージョンやIE11なども、TypeScript・Babelとポリフィルを利用すれば対応可能です(BigInt、for...inを除く。import()についてはwebpackも必要)参考
can i use
最新版TypeScript+webpack 5の環境構築まとめ
最新版で学ぶwebpack 5入門Babel 7でES2020環境の構築
- 投稿日:2021-01-29T15:18:03+09:00
[未経験エンジニア]のオリジナルアプリ制作の反省1.「ビューは先にしっかり作っとけ」
誰なのか
某短期集中エンジニア養成プログラムでエンジニア転職を目指している者です。
エンジニアとして学習を始めて、現在2ヶ月になります。何を書くのか
- オリジナルアプリを制作した過程で「もっとこうしておけばよかった...」と後悔した部分
- 「ここは、実装難しかったな...」と、思った部分
について,1日1つ取り上げ、ネタがなくなるまで投稿します。
基本的には、自分の備忘録としての記録となりますので、至らない点も多々あるかと思います。
それでも、もし僕みたいに「未経験からエンジニアになろう!」と思っている人の参考になれば幸いです。環境
ruby: 2.6.5
Rails: 6.0.3.4今回の結論
タイトルの通りです。ビューをあらかじめ作っておくことの重要性をひしひしと感じました。
ソースコードも交えてお話しします。こうしとけばよかった!
具体的には、開発を始める前の企画段階で
- リセットcssについての知識が足りておらず、とりあえずcssの設定を機能実装の都度ちょこちょこしていたこと
- レスポンシブデザインを考慮しない設計構造になっていたことの2つを課題として考えています。
なぜそうなってしまったのか
要件定義の段階で、実装したい機能は決まりました。しかし、それを支えてくれるページレイアウトに関しては、当初、フロントエンドに対して苦手意識もあり、「機能を開発する度に」html, cssを作成するやり方で進めていたのが、失敗の原因だと思います。一般的な開発ではどうなのか分かりませんが、これは自分にとって最終的に悪い形で帰ってきました。
今後に向けて考えたこと
個人開発、ポートフォリオ向けの開発には、webフレームワーク(有名なのはboostrap)などを用いて大体のレイアウトを整えるのも一つの手段だと思います。
しかし、今回あげた課題に共通するのは、そもそもwebアプリケーション全体としてのゴールを見通せてなかったことにあると思います。ゴールは、最初に決めておいた方がいいですよね。すなわち、要件定義の際に、ページレイアウト、画面遷移図などを、メモの時点でほとんど完成系に近づけるべきだということです。
このように提案する理由は、サーバーサイドとフロントエンドがお互いに影響を及ぼす可能性を排除できないことにあります。一つ例を挙げます。form_with, link_toのようなヘルパーメソッドのブラウザ上の表示は、html, cssの言語に変換されて表示されます。
例えば、form_withは、フォームを作成することができるヘルパーメソッドです。
<%= form_with url: "/posts", method: :post, local: true do |f| %> <%= f.text_field :title %> <%= f.submit '投稿する' %> <% end %>このように記述された場合、ブラウザ上では
<form action="/main" method="post"> <input type="text" name="title"> <input type="submit" value="投稿する"> </form>と表示されます。
以上から、サーバーサイドとフロントエンドは密接に関わり合っていることがわかってもらえたと思います。
アクションプラン
- ページレイアウト(ビュー)は、サーバーサイドやJavaScriptなど、レイアウトに影響を及ぼす要素を踏まえて完成度60~70%くらいまで企画で決めておき、先に実装を進めておく。
- リセットcssは、レイアウト全体(物によっては字体まで!)に影響を及ぼすので、一番最初に導入し、読み込ませる。
- レスポンシブなサイトになるように、企画の時点で、少なくとも「その箱の中でどのくらいの存在感を持たせるのかという割合(%」)」を決めておく。できるなら、もっと細かくpx単位で考えぬく。
- もしサーバーサイドとして活躍したいなら、時にはboostarpなどのフレームワークも用いる。
最後に
最後まで読んでいただいた皆様、ありがとうございます。
ソースコード、記事の書き方について「もっとこうしたほうがいいよ!」というご意見、「そこどうなっているの?」というご質問など、お待ちしております。参考文献
・【Rails】form_withの使い方を徹底解説!
https://pikawaka.com/rails/form_with・【個人開発・ポートフォリオに】簡単にいい感じのデザインにできるサービスまとめ
https://qiita.com/aiandrox/items/4196c8f5b564d29fdce7
- 投稿日:2021-01-29T12:06:54+09:00
NetCommons2のファイルアップロード時にでてくるダイアログについて
旧会社HPからの転記です。
ファイルアップロード時に、アップロード処理の時間が長いと
「アップロードが失敗したか、タイムアウトになった可能性があります。処理を続行しますか?」
とダイアログが出る事がありますが、
カスタマイズ時に非表示にできないか調べました。
https://github.com/netcommons/NetCommons2/blob/537e08b7a2892bacd2eed5dbac3ea45792de0f36/html/webapp/modules/common/files/js/common.js#L2451webapp/modules/common/files/js/common.js/*timeout_flag:0の場合、タイムアウトチェックをしない */ /*********************************************************/ sendAttachment: function(params_obj) {上記パラメータをセットすれば、できる。
サンプルJS
/** * ファイルアップロード */ uploadFile: function() { var messageBody = new Object(); messageBody["action"] = "custummodule_action_main_import"; var option = new Object(); option["param"] = messageBody; option["top_el"] = $(this.id); option["timeout_flag"] = 0; // タイムアウトチェックをしない option["callbackfunc"] = function(response) { commonCls.alert("インポートしました。"); commonCls.sendRefresh(this.id); }.bind(this); option["callbackfunc_error"] = function(file_list, res){ commonCls.alert(res); }.bind(this); commonCls.sendAttachment(option); },ではでは。
- 投稿日:2021-01-29T11:31:56+09:00
GASに入門して, Claspを使ったGit管理+Gitlab-CIを使ったCI / CDまでやった記録
GASに入門して, Claspを使ったGit管理+Gitlab-CIを使ったCDまでやってみた
今冬、初めて雪が窓の外をちらついた日のことだ。
開発部署に所属してこそいないものの、GASを使って業務の自動化をする仕事をしている人が、保守性・メンテナンス性が悪くて困っているという話をしていた。
「バージョン管理をしたらいいよ」というアドバイスが方々から飛んだ。私もそのアドバイスをしたうちの一人だった。そのとき、私はGASを触ったことが一度もなかった。
それから30分後、私はなぜかGASを触ることになっていた。何の偶然か、私はこのタイミングで突然ドライブの特定のディレクトリに存在する全てのスプレッドシートからあるカラムを取得する必要性に迫られたのだ。GASに入門する
私はGASとは何かを検索したのちに、まずは適当なスプレッドシートを作成し、その上に初めてのGASを作成した。
エディタは簡単に開いた。作成したスプレッドシート上でTools -> Script editorとクリックするだけだった。エディタ上には、
Code.gs
という謎の拡張子のファイルに空の関数が用意されていた。私はおもむろに入門サイトからハローワールドをコピペする。function myFunction() { Logger.log('Hello, GSS') }
Run
をクリックすると、画面下に実行ログが表示された。輝かしいこのコードこそ、私の初めてのGASコードである。
これで「GASも触ったことがないだなんて」と後ろ指を指される人生とはおさらばだ。1もう少し実用的な例を実装してみよう。スプレッドシートを新たに作成してみたい。
調べたところ、スプレッドシートに値をまとめて入れるには、二次元配列が便利なようだ。
何かちょうどいい二次元配列はないかと脳内にスキャンをかけると、引っかかったのは私のお気に入りのアイドルグループだった。/** * @returns {string} spreadsheetId */ function generateFavoriteIdols() { const spreadSheet = SpreadsheetApp.create('faborite-idols') const idols = [ // Lengths of all arrays have to match. ['Joshima', 'Kokubun', 'Matsuoka', 'Nagase', ''], // TOKIO ['Ohno', 'Matsumoto', 'Aiba', 'Ninomiya', 'Sakurai'], // Arashi ] spreadSheet.getActiveSheet().getRange(1, 1, 2, 5).setValues(idols) return spreadSheet.getId() }ついでに、生成したスプレッドシートからアイドルの名前を読み取る関数も書いてみよう。
これができれば、私が業務で求められていた仕事はこなせることだろう。/** * @param {string} spreadSheetId * @returns {string[][]} */ function readFavoriteIdols(spreadSheetId) { const spreadsheet = SpreadsheetApp.openById(spreadSheetId) return spreadsheet.getActiveSheet().getRange(1, 1, 2, 5).getValues() .map(l => l.map(e => String(e))) }Claspを使ってGit管理する
仕事は済んだ。しかし私の胸には、一時間前に無責任にも放った「Gitでバージョン管理するといいよ」という自らの言葉がいつまでも刺さっていた。
私はブラウザを開いて、「どうかいい感じの何かが出てくれ」というぼやっとした祈りと共に、検索欄に「gas git 管理」と入力した。無事にいくつかの検索結果を得た私は、Claspを使って先ほど作ったコードをGit管理することにした。
Claspのインストールと事前準備は下の記事を参考にした。claspでGASのソースをGit管理
https://qiita.com/zaki-lknr/items/b4954c222c1c1db92cafさっそく先ほどまで触っていたスクリプトエディタのURLからIDっぽいところをコピペしてきて
clasp clone
してみると、無事コードをローカルに引っ張ってくることができた。$ mkdir gas-sample && cd $_ gas-sample$ clasp clone *** Warning: files in subfolder are not accounted for unless you set a '.claspignore' file. Cloned 2 files. └─ appsscript.json └─ Code.js Not ignored files: └─ Code.js └─ appsscript.json Ignored files: └─ .clasp.json
.gs
だった拡張子が、.js
になっている。これは触れてやらないのが気遣いというやつだったかもしれない。いくつかのファイルを落とせたら、次はこれをgitで管理する。
ユーザ名とメールアドレスは何でもいいが、自分のものを使うのが無難だ。gas-sample$ git init gas-sample$ git config user.email 'me@example.com' gas-sample$ git config user.name 'me' gas-sample$ git add appsscript.json Code.js gas-sample$ git commit -m 'initial commit'ところで、
.clasp.json
の中を見たところ、私が先ほどclasp clone
するときに使ったスクリプトIDが書いてあった。
これはCDの中で設定した方が後々便利そうなので、ここではGit管理しないことにした。clasp.json{"scriptId":"***"}ここまででバージョン管理システムの導入が完了した。私の胸は歓喜に打ち震えている。
バージョン管理システムを導入したからには、何か修正を入れてみたい。私はもう一つアイドルグループを追加することを思いついた。
const idols = [ // Lengths of all arrays have to match. ['Joshima', 'Kokubun', 'Matsuoka', 'Nagase', ''], // TOKIO ['Ohno', 'Matsumoto', 'Aiba', 'Ninomiya', 'Sakurai'], // Arashi + ['', '', '', '', ''], // SMAP ] - spreadSheet.getActiveSheet().getRange(1, 1, 2, 5).setValues(idols) + spreadSheet.getActiveSheet().getRange(1, 1, 3, 5).setValues(idols) return spreadSheet.getId()const spreadsheet = SpreadsheetApp.openById(spreadSheetId) - return spreadsheet.getActiveSheet().getRange(1, 1, 2, 5).getValues() + return spreadsheet.getActiveSheet().getRange(1, 1, 3, 5).getValues() .map(l => l.map(e => String(e)))できた。
さっそくこの修正をGitに入れて、デプロイしてみよう。gas-sample$ git add Code.js gas-sample$ git commit -m 'Add SMAP to idols' gas-sample$ clasp pushはやる気持ちを抑えてブラウザでスクリプトエディタをリロードすると、無事修正が反映されていることが確認できた。
Gitlab CIを使って自動でデプロイする
人は、バージョン管理ができるようになったら、次はCI / CDを回したくなるものだ。
私も一人の矮小な人間にすぎないということだろう。次の瞬間、私はGitlabを開き、新たにリポジトリを作成していた。2GitlabでClone with SSLの横に書かれたURLをコピーし、これをローカルのリポジトリで
origin
という名前に関連付ける。
masterブランチをoriginのmasterブランチに対してpushすれば、Gitlabが私のコードを管理してくれるようになる。gas-sample$ git remote add origin git@gitlab.com:me/gas-sample.git gas-sample$ git push -u origin masterところで、Gitlabでは
.gitlab-ci.yml
というファイルをコミットすると自動的にCI / CDを回してくれるようになる。
つまり、.gitlab-ci.yml
というファイルを作り、そこにclasp push
を実行するよう書くだけで、Gitlabに対してgit push
するとGASが自動的に更新されるようになるのだ。とりあえず
clasp push
するだけの.gitlab-ci.yml
を書こう。
認可情報とデプロイ先のスクリプトIDを環境変数へ逃がしたため、予めGitlabのリポジトリの設定 -> CI/CD -> Variablesに認可情報をCLASPRC_JSON
, スクリプトIDをSCRIPT_ID
として入力することが必要だ。3.gitlab-ci.ymlstages: - upload upload: stage: upload image: node:10.23.2-alpine3.11 script: # Never hard-code .clasprc.json, EVER - cp $CLASPRC_JSON ~/.clasprc.json - echo "{\"scriptId\":\"${SCRIPT_ID}\"}" > .clasp.json - npx @google/clasp push特に、認可情報(
~/.clasprc.json
)の流出には気を付けなければならない。これは決してGitで管理してはならない情報だ。~/.clasprc.json
がログなど見えるところに残っていることに気がついたら、すぐにトークンを無効化しよう。Apps with access to your account
https://myaccount.google.com/permissions?pli=1これでCI / CDを実現することができる。
さっそくpushして、Gitlab上でパイプラインが眺めるのを見てみよう。gas-sample$ git add .gitlab-ci.yml gas-sample$ git commit -m 'add .gitlab-ci.yml' gas-sample$ git pushGitlab CIの実行ログには、"Job succeeded"の美しい文字が並ぶ。
これでGitlabにpushするだけで全てが完結するようになった。Executing "step_script" stage of the job script 00:22 $ cp $CLASPRC_JSON ~/.clasprc.json $ echo "{\"scriptId\":\"${SCRIPT_ID}\"}" > .clasp.json $ npx @google/clasp push npx: installed 160 in 16.186s └─ Code.js └─ appsscript.json Pushed 2 files. Cleaning up file based variables 00:00 Job succeededここまでくれば、unittestの自動実行やAltJSを使った開発だって簡単だ。
あの微妙に使い勝手の良いスクリプトエディタへの未練を捨て去り、普段使っているIntellij ideaやVSCodeで開発をしてもいいのだ。もちろんvimやemacsを使ってもいい。最後に、Gitlab-CIの環境変数のオーバーライドを利用して、別のスクリプトIDに対してデプロイを試してこの記事を終わりにしよう。
これができれば、開発環境・本番環境の切り替えができるだろう。新たにスプレッドシートを作り、Tools -> Script editorをクリックする。URLのIDっぽいところを控えておく。
Gitlabに行き、ロケットのアイコンからPipelinesを開いて、Run Pipeline
をクリックする。
VariablesにSCRIPT_ID
をvariable key, 控えておいたScriptIdをvariable valueとして、Run Pipeline
をクリックする。
緑色のチェックとpassedという文字を深い満足をもって確認する。スクリプトエディタをリロードすると、真っ白だったエディタの中には、先ほど作成したお気に入りのアイドルグループ表を作成するスクリプトが表示されていた。
- 投稿日:2021-01-29T11:23:00+09:00
typeORMでセキュアなカラムをselectさせない
typeORMで、パスワード等のカラムを扱う時にうっかりapiのレスポンスに値を含め無い様にする仕組みが用意されています。
何もしない場合
@Entity() export class User { @PrimaryGeneratedColumn() readonly id: number; @Column({ name: 'name', length: 255, }) name: string; @Column('varchar', { name: 'password'}) password: string; }selectすると全カラムが普通に取得されます。うっかりそのままAPIのレスポンスに含めるとまずいです。
除外する
@Entity() export class User { @PrimaryGeneratedColumn() readonly id: number; @Column({ name: 'name', length: 255, }) name: string; @Column('varchar', { name: 'password', select: false}) // ここ password: string; }
{select: false}
というオプションをつければOKです。SQLレベルで除外されます。ちなみに何かしらの理由で敢えて取得したい場合は、明示的に書けばOKです。
const user = await this.userRepository.find({ select: ['password'] });参考
- 投稿日:2021-01-29T11:08:44+09:00
reactのmaterial-tableで一部カラムのみを編集可能にする
Material-table
https://material-table.com/#/
Material-uiに則ったテーブルレイアウトを簡単に作成できるライブラリ。一部のカラムを編集できるデータの一覧表を実装したくて採用。編集可能にしてみる
https://material-table.com/#/docs/features/editable
には「Row Editing」(行単位の一括編集)と「Cell Editing」(セル単位での編集)について記載があった。「Row Editing」の方は行の全カラムが編集可能になってしまい、編集可能にする必要のないカラムがあることから、「Cell Editing」の方かなと思い実装してみる。const columnList = [ { title: "ID", field: "id" }, { title: "name", field: "name" }, { title: "description", field: "description" }, ]; const dataList = [ { id: 1, name: "aaa", description: "xxxxxxxxxx" }, { id: 2, name: "bbb", description: "yyyyyyyyyy" }, { id: 3, name: "ccc", description: "zzzzzzzzzz" }, ]; <MaterialTable columns={columnList} data={dataList} cellEditable={{ onCellEditApproved: (newValue, oldValue, rowData, columnDef) => { return new Promise(() => { // do something }); }, }} ></MaterialTable>結果、セル単位での編集にはなったものの、全セルが編集可能になってしまった。(ID列は編集させたくない)
一部のセルだけ編集可能にするには…
const columnList = [ { title: "ID", field: "id", editable: "never" }, { title: "name", field: "name" }, { title: "description", field: "description" }, ];editable: "never"
をカラムの定義につけてあげると、そのカラムは編集不可になった。(ID列をクリックしても編集欄が表示されないけど、それ以外の列は編集可能)
参考になれば幸いです。
- 投稿日:2021-01-29T10:26:39+09:00
LaravelでFullcalendarを実装する方法
はじめに
今回はLaravelでFullcalendarを実装する方法について解説します。
LaravelでFullcalendarを実装するにあたり、参考記事があまりなく、実装に苦労したので、同じような方の助けになればと思います。-各バージョン
-Laravel 6.x
-PHP 7.4.9
-MySQL 5.7.30
-Fullcalendar v5Fullcalendarはバージョンによって記述方法が異なるので注意してください。
v4の記事を参考にしてもうまくいかないことが多かったです。Fullcalendarをダウンロードする
公式ドキュメントよりダウンロードできます。
https://fullcalendar.io/docs/getting-startedNPMやCDNを利用する方法がありますが、ダウンロード方法はお好きな方法で問題ないです。
今回はzipファイルをダウンロードする方法し、以下のように追加しました。event.blade.php<!-- fullcalendar dependencies --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.0/moment.min.js"></script> <!-- fullcalendar script --> <script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.0.1/fullcalendar.min.js"></script> <!-- fullcalendar style --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.0.1/fullcalendar.min.css">モデルとデーブルを作成する
わたしの場合はModelsファイルの中にモデルを格納したいので、以下のように書いています。必要がなければ、Modelsのところはカットして問題ないです。
php artisan make:model Models/Event -mまずはテーブルから書いていきます。
Fullcalendarには様々なプロパティが用意されていて、以下のサイトより確認することができます。
https://fullcalendar.io/docs/event-parsing
今回はシンプルに、予定のタイトルと登録する日付、文字の色を変更できるように設定していきます。events.table.phppublic function up() { Schema::create('events', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('title', 100); $table->date('start'); $table->string('textColor'); $table->timestamps(); }); }マイグレーションします。
php artisan migrateカレンダーに登録したデータを表示する
カレンダーの表示とカレンダーに登録したデータを表示するControllerとルーティングを書いていきます。
php artisan make:controller EventControllerindexアクションにカレンダーの表示とDBに保存されているデータをカレンダーに表示させるコードを書きます。
今回はDBファサードを利用するので、useで宣言が必要です。EventController.phpuse App\Models\Event; public function index(Request $request) { if ($request->ajax()) { $data = DB::table('events')->select('title', 'start', 'textColor')->get(); return response()->json($data); } return view('/event'); }
$request->ajax()
でデータがajax通信かどうかを確認できるメソッドです。
if内では、eventsテーブルからデータを持ってきて、response()->json()
を利用し、json形式で保存するようにしています。
このようにすることによって、PHPで保存したデータをJavaScriptで利用できるようになります。続いて、ルーティングは以下のように設定します。
web.phpRoute::get('/index', EventController@index);カレンダーの情報を登録する
カレンダーの情報を登録するためのコードを先ほど作成したEventControllerに書いていきます。データの登録後、カレンダーが表示されているページに戻るようにしています。
EventController.phppublic function store(Request $request) { $event = new Event; $event->title = $request->input('title'); $event->start = $request->input('start'); $event->textColor = $request->input('textColor'); $task->save(); return redirect('/event'); }続いてルーティングです。
web.phpRoute::post('/store', EventController@store);カレンダーの表示と登録フォームを設定する
カレンダーの表示自体はとても簡単で、カレンダーを表示させたい場所に以下のコードを書くだけで表示できます。
event.blade.php<div id="calendar"></div>フォームはCSSを考慮せずに書いているのでご了承ください。
event.blade.php<form method="POST" action="{{ route('/store') }}"> @csrf <input type="text" name="title"> <input type="date" name="start"> <input type="color" name="textColor"> <button type="submit">登録</button> </form>データの登録なので、methodは
POST
を指定し、actionにはフォームが送信されたらEvercontrollerのstoreアクションにつながるようにしたいので、先ほどweb.phpで指定したroute('store')
を指定しています。type属性にはそれぞれ入力方式に適したものを入力し、name属性にはeventsテーブルで指定したカラム名を入力します。
続いてはJSを書いていきます。
今回はevent.blade.phpに直接書きましたが、便宜JSだけディレクトリを分けてもいいかと思います。event.blade.php<script> $(document).ready(function () { $('#calendar').fullCalendar({ // はじめりの曜日を月曜日に変更 デフォルトは日曜日になっており、日=0,月=1になる firstDay: 1, headerToolbar: { right: 'prev,next' }, events: '/home', }); }); </script>
events
にはカレンダーに保存している情報を表示するアクションが書かれている、EventControllerのindexメソッドにつながるように書く必要があります。
そのため、先ほどweb.phpで指定した/home
を指定しています。今回はシンプルなカレンダーになっていますが、Fullcalendarには様々なオプションが用意されています。
簡単なものだと、headerToolbarはrightだけではなく、left,centerのカスタマイズもできますし、eventをドラッグしたり、日にちを選択して登録することなんかもできます。
詳しいオプションに関しては、Fullcalendarの公式マニュアルをご確認ください。
https://fullcalendar.io/docs#tocこれで、カレンダーにデータの登録と、登録したデータの表示ができるようになりました!
さいごに
今回は登録フォームを直接書いていますが、今後は日付から選択をしてデータの登録をしたりなど、いろいろとカスタマイズをしてみたいと思います!
最後まで読んでいただきありがとうございました!
- 投稿日:2021-01-29T09:33:31+09:00
【備忘録】Javascriptのshift, slice, spliceの違い
shift
shiftを使った配列から1要素目を削除して、戻り値としてその要素を返す。
元配列への影響:有let a = [1,2,3]; let b = a.shift(); //a=[2,3] b=[1]slice
slice(start, [end])は
start: 取り出す先頭のインデックス
end: 取り出す最後のインデックス+1
元配列への影響:無let a = [1,2,3]; let b = a.slice(1); //a=[1,2,3] b=[1] let b = a.slice(0,1); //a=[1,2,3] b=[1] let b = a.slice(1,2); //a=[1,2,3] b=[2]splice
slice(start, number)は 配列の一部を削除する。
(実際には置換や挿入もできる。ここでは比較のため削除のみ)
start: 取り出す先頭のインデックス
number: 取り出す要素数
元配列への影響:有let a = [1,2,3]; let b = a.splice(0,1); //a=[2,3] b=[1] let b = a.splice(1,2); //a=[1] b=[2,3]
- 投稿日:2021-01-29T09:26:37+09:00
Node.js で Azure BLOB オブジェクト を 30秒間 だけ Read 可能な SAS トークン を ライブラリを使わないで生成する
公式ドキュメント : Create an account SAS
BLOB オブジェクト を 30秒 だけ Read 可能なURL
const crypto = require('crypto'); function truncateIsoDate(date) { return date.toISOString().substring(0, 19) + 'Z'; } const getURL = (account, container, blob) => { const url = `https://${account.name}.blob.core.windows.net/${container}/${blob}`; const STORAGE_ACCOUNT_NAME = account.name; const ACCOUNT_ACCESS_KEY = account.key; const now = new Date().getTime(); const start = new Date(now - 5 * 60 * 1000); // 5秒前(時計のズレ考慮) const end = new Date(now + 30 * 60 * 1000); // 30秒間 const signedpermissions = 'r'; const signedversion = '2018-03-28'; const signedservice = 'b'; const signedresourcetype = 'o'; const signedstart = truncateIsoDate(start); const signedexpiry = truncateIsoDate(end); // const signedpermissions = 'rwdlac'; // const signedresourcetype = 'sco'; // const signedIP = '0.0.0.0/0'; // const signedProtocol = 'https'; // 順番は厳守 const stringToSign = [ STORAGE_ACCOUNT_NAME, signedpermissions, signedservice, signedresourcetype, signedstart, signedexpiry, '', // signedIP , '', // signedProtocol, signedversion, '', // (末尾の改行必須) ].join('\n'); const key = Buffer.from(ACCOUNT_ACCESS_KEY, 'base64'); const hmac = crypto.createHmac('sha256', key); const sig = hmac.update(stringToSign, 'utf8').digest('base64'); // 順番はフリー const sas = [ `sp=${signedpermissions}`, `ss=${signedservice}`, `srt=${signedresourcetype}`, `st=${signedstart}`, `se=${signedexpiry}`, // `sip=${signedIP}`, // `spr=${signedProtocol}`, `sv=${signedversion}`, `sig=${encodeURIComponent(sig)}`, ].join('&'); return [url, sas].join('?'); }; const account = { name: 'ストレージアカウント名', key: 'キー ...==', }; const url = getURL(account, 'コンテナ名', 'ブロブ名'); console.log(url);シグネチャーに container や blob の情報が含まれないから、この SAS トークンは全部のオブジェクトで共通、ということですね。
01/29 追記
ちなみに、az コマンドで同じことをやるには、こんな感じ
ACCOUNT_NAME='ストレージアカウント名' ACCOUNT_KEY='キー ...==' FUTURE_DATE=$(date -v+30M '+%Y-%m-%dT%H:%MZ') az storage account generate-sas \ --permissions r \ --resource-types o \ --services b \ --expiry $FUTURE_DATE \ --account-name $ACCOUNT_NAME \ --account-key $ACCOUNT_KEY
- 投稿日:2021-01-29T01:15:39+09:00
Context / useContext を書きながら学ぶ
Reactの組み込みフックである
Context
とuseContext
の説明をします。また、
useContext
は、ReactのContext
と併用するので、Context
の解説もします。Contextとは
useContext
は、Context
というReactの仕組みを利用するために必要なフックです。なので、まずは
Context
について説明します。
Context
とは...と言っても様々な言い方ができるでしょう。
- 「状態」と「状態を変更するメソッド」を、propsを用いず、アプリケーション全体で取り回すことができるやつ
- Propsを利用せずに、様々な階層のコンポーネントに値を共有するReactのAPI
などなどですね。図で説明しましょう。
本をReactで表現するとします。
ホントは、もっといろいろなコンポネントがありえますが、今回は、Bookのデータ(仮に
bookData
とする)を<Page />
と<Title />
で、使いたいとします。すると、このようにpropsをバケツリレーして渡す方法もあります。ですが、
<Body />
と<Cover />
はそんなデータを使用しない。とか、もっと多階層で中間のコンポネントはbookData
をまったく使わないので、バケツリレーで渡す意味がない時ありますよね?もしくは、グローバルにどこでも取りまわせるstateを持ちたい時がありますよね?
そんな時は、
Context
の出番です。
Context
がデータストアの役割をはたし、propsを使わず直接bookData
を下層階層のコンポネントが使うことができるようにしたものです。
useContext
は、Context
の機能をさらにシンプルに使うことができるやつです。Contextを利用するために必要なもの
- Contextオブジェクト:
React.createContext
というReactのAPIの戻り値- Provider: Contextオブジェクトが保持しているコンポネント
- Consumer: Contextオブジェクトから値を取得しているコンポネント
サンプルコード
基本の状態は以下にします。(Bookを表すには雑すぎますが...ツリー構造さえわかればOKなので許してください)
Contextを利用して、下層コンポネントに、データを受け渡します。
以下の順番で書いていきます。
- createContext(
BookContext
)してexportしておく(Book コンポネントで実装)- Providerコンポネントを作成し、valueにオブジェクト(state)をセットします(Book コンポネントで実装)
- 下層コンポネントで、先ほど作成したBookContextをimportします(Title コンポネントで実装)
- importしたBookContextを使ってconsumerを作成(Title コンポネントで実装)
- Consumerコンポネント内でbook state の中身にアクセスできます
赤枠が、変更点です?
実行結果はこんな感じです。
propsで上の階層から渡していないのに、下階層でcreateContextしたデータが引っ張れていることがわかります。
それでは次に、
useContext
を使っていきましょう。useContextとは
useContext
を使った場合でも、Providerを使って値を渡す点は同じです。構文はこんな感じです。
const Contextオブジェクトの値 = useContext(Contextオブジェクト)Contextオブジェクトから、取得できる値は、Contextオブジェクトが保持しているProviderのvalueプロパティに指定された値です。
サンプルコード
<Cover />
とその子供の<Title />
は、createContextされたBookContext
とAuthorContext
コンテキストをPrivide
されているので、それぞれのデータを引っ張ることができます。このように、複数のコンテキストを扱うことも可能です。
サンプルコード
/components/Book.jsimport React, { createContext, useState } from "react"; import Cover from "./Cover"; import Body from "./Body"; const bookData = { author: "ryosuketter", title: "how to use React context", isbn: 12345, yearOfPublication: 2021 }; const authorData = { name: "ryosuketter", age: 35, gender: "male" }; export const BookContext = createContext(); export const AuthorContext = createContext(); const Book = () => { const [book, setBook] = useState(bookData); const [author, setAuthor] = useState(authorData); return ( <> <BookContext.Provider value={book}> <AuthorContext.Provider value={author}> <Cover /> </AuthorContext.Provider> <Body /> </BookContext.Provider> </> ); }; export default Book;components/Cover/Title.jsimport React, { useContext } from "react"; import { BookContext, AuthorContext } from "../../Book"; const Title = () => { const book = useContext(BookContext); const author = useContext(AuthorContext); return ( <div> <p>this is 「{book.title}」</p> <p>author is {author.name}</p> <p>age is {author.age}</p> </div> ); }; export default Title;特定のコンポネントで、Contextの追加や上書きをしたデータを作成して使うことも可能です。
サンプルコード
/components/Book.js// 上と同じ
components/Body/Page.jsimport Reac, { useContext } from "react"; import { BookContext } from "../../Book"; const Page = () => { const book = useContext(BookContext); const customedBookContext = { ...book, author: "ryosuke", publisher: "Qiita publications" }; return ( <div> <p>this is page</p> <p>author is {customedBookContext.author}</p> <p>publisher is {customedBookContext.publisher}</p> </div> ); }; export default Page;上2つのコードの実行結果
Context利用時の注意点(Context更新時に不要な再レンダーを招く)
Contextは使い方によっては、パフォーマンスの問題を引き起こす可能性があります。
なぜから、Provider内のすべてのConsumerは、Proverのvalueプロパティが更新するたびに再レンダリングするからです。
特に、以下の場合は注意です
- 再レンダリングされる Consumer の数が多い場合
- Consumerの子コンポネントのレンダリングコストが高い場合
不要な再レンダリングを防ぐ方法は、次の3つです。
- Contextオブジェクトの分割
- React.memoの利用
- useMemoの利用
ぜひ、試しながらやってみてください。
- 投稿日:2021-01-29T00:24:43+09:00
Quill editor 画像をS3にアップロードし表示する方法
使用言語
・PHP(Laravel)
・javascript(jQuery)
・HTML使用ライブラリ
https://quilljs.com/実装したい機能
画像を選択 → apiを叩く → s3にアップロード → s3のパスで画像を表示※Quillのeditor内に画像を挿入すると、
デフォルトだと、ローカルのパスで画像が表示される。作業フロー
①quill jsを読み込む
②javascriptを書く(ajax通信を行う)
③PHP(laravel)でajaxのリクエスト受け取ってs3に保存などもろもろ行う
④apiのルート確保①quill jsを読み込む
https://quilljs.com/
公式サイトよりドキュメント参照。↓ページquill jsクイックスタート
https://quilljs.com/docs/quickstart/<!-- Include stylesheet --> <link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet"> <body> <!-- エディター部分 --> <form> <div id="editor" style="height: 200px;"></div> <input type="hidden" name="main" id="project_contents_inner"> <button type="submit" name="subbtn">投稿</button> <form> </body> <!-- Include the Quill library --> <script src="https://cdn.quilljs.com/1.3.6/quill.js"></script> <!-- 後ほど書くjavascript読み込む --> <script src="{{ asset('/js/main.js') }}"></script>②javascriptを書く(ajax通信を行う)
main.js//ここのツールバーはカスタムできます。 var toolbarOptions = [ ['bold', 'italic', 'underline', 'strike'], // toggled buttons [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown ['link'], ['image'], ['video'], [{ 'list': 'ordered' }, { 'list': 'bullet' }], // superscript/subscript [{ 'align': [] }], [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme ['clean'] // remove formatting button ]; const editor = new Quill('#editor', { bounds: '#editor', modules: { toolbar: this.toolbarOptions }, placeholder: 'なんか書いてー ', theme: 'snow' }); // /** // * Step1. select local image // * // */ function selectLocalImage() { const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('name', 'up_file'); input.click(); // Listen upload local image and save to server input.onchange = () => { const file = input.files[0]; // file type is only image. if (/^image\//.test(file.type)) { saveToServer(file); } else { console.warn('You could only upload images.'); } }; } // /** // * Step2. save to server // */ function saveToServer(file) { /* Ajax経由で画像登録 */ var fd = new FormData(); fd.append('up_file', file); // 画像 $.ajax({ url: 'https://hoge/upload/image', // 画像登録処理を行うPHPファイル(api) type: 'POST', data: fd, cache: false, contentType: false, processData: false, }).done(function (data) { const url = data.path; console.log(data) insertToEditor(url); }).fail(function (jqXHR, textStatus, errorThrown) { console.log('ERROR', jqXHR, textStatus, errorThrown); }); return false; } // /** // * Step3. insert image url to rich editor. // * // * @param {string} url // */ function insertToEditor(url) { // push image url to rich editor. const range = editor.getSelection(); editor.insertEmbed(range.index, 'image', url); } // quill editor add image handler //画像が選択されたら↑の関数がstep1~順番に走る editor.getModule('toolbar').addHandler('image', () => { selectLocalImage(); }); //プロジェクトを投稿する際にエディター内のものを#project_contents_innerに入れる。 $(function () { $("#project_form").on("submit", function () { $("#project_contents_inner").val($(".ql-editor").html()); }) })③PHP(laravel)でajaxのリクエスト受け取ってs3に保存などもろもろ行う
php artisan make:controller Api/ProjectUploadController<?php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; class ProjectUploadController extends Controller { public function upload_image(Request $request) { $filePath = '/picture/contents'; $image = $request->file('up_file'); $t = Storage::disk('s3')->putFile($filePath, $image, 'public'); $imagePath = Storage::disk('s3')->url($t); return response()->json([ 'message' => 'ok', 'path' => $imagePath, ], 200, [], JSON_UNESCAPED_UNICODE); } } ※s3への保存方法は別途調べてください。④apiのルート確保
api.phpRoute::post('hogehoge/image/upload','Api\ProjectUploadController@upload_image');完成
これでeditor内部にs3からのパスで画像を表示できているでしょう。
あとはquillで作成したHTML入りの文章をDBに保存するなりで大丈夫でしょう。動かなかったら質問お願いします。
日本語の記事見かけてないので書きました。