20210129のJavaScriptに関する記事は25件です。

音のあるプッシュ通知を実装してみた(React.js)

はじめに

最近、個人開発でプッシュ通知を実装して、音をつけたいなと思ったので実装してみました。プッシュ通知はNotificationAPIを使っているので、残念ながらサイトを開いてないと通知されません。ServiceWorkerとweb-push使って、バックグラウンドで実装できるようにしたいと思ってます。
React使ってます。

参考:
通知APIの使用
Web Audio API

効果音探しに使わせていただきました↓
フリー音楽素材魔王魂

ソースコード

説明は簡単にコメントで記述しました!

src/App.tsx
import { 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;

初回アクセス時は下の画像のようなものが出てくるので許可するとプッシュ通知が出るようになります。
image.png

creact-react-appで作られたApp.tsxを変えただけの簡単なコードです。
押したらプッシュ通知を出すボタンと音の有無を切り替えるボタンが表示されます。
image.png

一応完成のコード公開したので実際に試したい場合はクローンしてみてください!
https://github.com/NozomuTsuruta/simple-notification-audio

終わりに

ここまで読んでいただきありがとうございました!少しでもお役に立てれば嬉しいです
また、「もっといい方法あるよ」だったり、「こういう面白い技術があるよ」などなど気軽にコメントやTwitterで絡んでくれたりすると嬉しいです!
いろいろな技術を触りながら日々精進していきます!

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

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

以上です。

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

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


  1. ファイルをそのままChromiumで開いたら文字化けしなくても、なぜかData URI schemeだと化ける。なぜだ? 

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

【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.91
function 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>

実行結果
image.png

参考文献

JavaScriptでスマホ判定する2つの方法
JavaScriptでアクセス元の端末がスマホかどうか判定する方法を現役エンジニアが解説【初心者向け】

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

マウスストーカーを実装する方法

サンプル

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.

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

firebase deploy で詰まった時の対処法

簡単な自己紹介

今回が初投稿になります!現在フロントエンド を学習している就活生です。
何事もアウトプットが重要だと思うので投稿してみようと思いました!かつ自分の備忘録として記録しようと思います。

今回vueCLIとfirebaseを使用してアプケーションを作成したのですが、最後の最後デプロイするところで壁にぶつかったので記録しておきます!

サイトが公開されていない

詳しいデプロイ方法は他の記事を参照してください。

$ firebase deploy

これでfirebase deployが成功した場合、最後にURLが発行されると思います。
そしてそのURLをクリックして無事公開できたと思ったらこの画面になりました、、、
Image from Gyazo

原因

おそらく人によって様々かとは思いますが私の場合は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 を実行すると成功しました。

これまでターミナルなんて物を触ったこともなかったのでこんなこと当たり前だよ!と言われそうですが、
何故無知な者で。。
これからも精進していきます!

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

【React】Reducerでstateを直接変更してはならないことの背景

Reducerのルール

Reducerにはいくつかの決まりごとがあり、よくあるミスとして「引数として渡されたstateそのものに変更を加えてはならない」ところを、直接変更を加える処理をreducerに記述してしまうというものがあります。

ルールの背景

各reducerをまとめてexportするときにcombineReducers関数を用いますが、このcombineReducersのコード( https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts )を確認すると、終盤辺りに以下のようなコードがあります。

combineReducers.ts
let 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の決まりごとに関してここで学んだ内容について、備忘録を兼ねて共有させていただきました。

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

「ジャバスクリプト」関数定義

関数とは

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 変数名 = () => {
  処理
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「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 変数名 = () => {
  処理
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「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の構文
ダブルクォートやシングルクォートの代わりにバッククォートで囲むことで、文字列内に挿入することができる。
image.png

要は

文字列の中に変数を埋め込むことができる
改行を入れることができる
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){
  // 処理の記述
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
}

hoto7.png

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
}

hoto8.png

なぜ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)
    }
    //追加ここまで*******************

}

hoto9.png

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  //今読み込んでいるレーザーを動かす
    }

hoto12.png

今回のコード

index.html
<!--前回と同じなので省略-->
main.js
var 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();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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個開いているんですよね。

image.png

Puppeteerのドキュメントを見ると、browser.pages()でページ一覧が取れるので、

const browser = await puppeteer.launch({ headless: false, });
const page = (await browser.pages())[0];
await page.goto("https://www.google.com/");

こうすると、もとからあるタブを使ってくれました!

image.png

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

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.js
import 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.js
import 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■でき上がったカウンター

2009001_001.png

react-useのuseKeyフックを使う

では、いよいよuseKeyフックを使います。まずは、アプリケーションへのreact-useのインストールです。

npm install react-use

そうしたら、キーボードイベントを扱いたいコンポーネントに、useKeyimportしてください。上下の矢印キーで、カウンターを増減したいと思います。

useKeyの使い方は、つぎのように気が抜けるほど簡単です。第1引数にキーを示すKeyboardEvent.keyプロパティの値、第2引数にイベントハンドラを渡します。JSXのどの要素にイベントハンドラを加えるか、などと悩まなくて済むのです。

src/App.js
import { useKey } from 'react-use';

function App() {

    useKey('ArrowDown', decrement);
    useKey('ArrowUp', increment);

}

ここまでのルートモジュールsrc/App.jsの記述全体を、つぎのコード003にまとめました。

コード003■上下矢印キーでカウンターを増減させるルートモジュール

src/App.js
import 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.js
function App() {

    const predicate = (event) => event.ctrlKey && event.key === 'Escape';
    const escKeyUpHandler = () => setCount(0);
    useKey(predicate, escKeyUpHandler, {event: 'keyup'});

}

コード004■[control]/[Ctrl] + [esc]キーでカウンターをリセットするルートモジュール

src/App.js
import 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;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでほんの少しハイレベルな検索機能を作ってみた。(初心者向け)

こんにちは、しろうです。

現在、Laravelで小説サイトを作成しています。

そんな中で、(自分的には)ちょっとだけハイレベルな検索機能を作ってみたので、忘れないようにメモとして残しておきます。

検索機能を実装したい人はぜひ、参考にしてください。

もしミスとかあればコメントして頂けると大変有り難いですm(_ _)m

前置き

・クラス名とかひどいのあるかと思いますが、気にしないで頂けると幸いです。
・Formファサードを使ったり、素のformを使ったり混在していますが、許してくださいm(_ _)m
・cssは貼り付けていないので、皆さん側で好きなように変更してください。

今回作る検索機能

・複数条件での絞り込み検索ができる。(例:キーワードとジャンルで絞り込む。)
・現在の検索条件がタグとして表示される。
・タグを削除すれば、検索条件から削除される。

検索機能のイメージ動画

ちなみにイメージ動作は下記の通りです。

ezgif.com-gif-maker.gif

下記のように、他のページからジャンルとかをクリックしたら、そのジャンル名で検索してくれる機能もつけてます。

ezgif.com-gif-maker (1).gif

さて、じゃあどんどん作っていきます。

とりあえず検索機能を作る

viewファイルの作成(検索項目の部分)

スクリーンショット 2021-01-29 13.53.31.jpg

とりあえず検索項目を作成していきます。

今回は「キーワード検索」と「ジャンル検索」のみを作成しています。
また、CSSは貼り付けないので、皆さんの方で好きなように変更よろしくお願いしますm(_ _)m

search.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.php
Route::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.php
public 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)}}とかで中身を確認してみてください。

現在の検索条件を表示する機能の作成

スクリーンショット 2021-01-29 14.04.43.jpg

なんて説明すればいいかわかりませんが、ここからは上記の画像のやつを作っていきます。笑

方法としては、そこまで難しくありませんので、ご安心ください!!!

まずは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メソッドで引数に与えたものと一致するパラメーターを削除します。

例えば、attrkeywordという文字列が入っている場合は、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>

並び替え機能も実装

スクリーンショット 2021-01-29 16.15.34.jpg

ついでに並び替え機能の紹介もしておきます。(「新着順」と「更新順」だけ)

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.php
public 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

たぶん、もう少し仕様変更とかしますが、ひとまずこれにて終了です!お疲れ様でした。

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

ES2020から追加された機能

ES2020から追加された機能について

今回は、javascriptの最新バージョンES2020について、まとめました。


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がある場合
Elements.png

・cityない場合
Elements Console Sources Network ».png

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(); 

▼ 実行結果

Elements Console.png
このように、すぐに読み込まれてしまう。

▼ 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秒後にコンソールするようにセットした結果

Elements.png
しっかりと、読み込んだモジュールを遅延できている。

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}`)
  );

▼ 実行結果

Console What's New.png
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  );

▼ 実行結果

Default levels.png
期待どうりの処理がされません。

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);
  }
);

▼ 実行結果

resolve.png
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)); 

▼ 実行結果

Console What's New.png
falseも値なしと処理されている。

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)); 

▼ 実行結果

Console What's New.png
falseはしっかりと文字列として処理されている。

2^53以上の整数を扱えるBigInt

BigInt は組み込みオブジェクトで、 Number プリミティブで表現できる最大の数、 Number.MAX_SAFE_INTEGER (2^53)よりも大きな数値を信頼できるものとして表現する方法を提供します。 BigInt は任意に巨大な整数に使用することができます。
 Number.MAX_SAFE_INTEGER (2^53)以上の値を計算しようとすると、計算結果に誤差が生じます。
その誤差をなくすために使うのが、BigIntです。

▼ 計算結果に誤差が生じる例

console.log(9007199254740991 + 2)

期待する値は9007199254740993です。

▼ 実行結果

Console What's New.png
1のズレが生じてしまう。

BitIntを使うと、2^53以上の整数も扱えるようになります。BigIntを扱うには、BigInt(数値)とするか、数値nという記述をします。BigIntはBigInt同士でしか計算ができないので、Numberとあわせて使う場合はNumberもBigIntに変換するようにする。

▼ BigIntの計算例

console.log(BigInt(Number.MAX_SAFE_INTEGER) + 2n);
console.log(9007199254740991n + 2n)

▼ 実行結果

Console What's New.png
今回はしっかりと期待する値が、出ました。
また、出力された9007199254740993nは数学世界での整数9007199254740993を指します。

気をつける点として、BigInt はBigDecimal ではないため、演算結果は 0 の方向に丸められます。別の言い方をすれば、小数を返すことはありません。

▼ 小数点が返されない例

const expected = 4n / 2n;
const rounded = 5n / 2n;

console.log(expected);
console.log(rounded)

▼ 実行結果

Console What's New.png

本来であれば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環境の構築

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

[未経験エンジニア]のオリジナルアプリ制作の反省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

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

NetCommons2のファイルアップロード時にでてくるダイアログについて

旧会社HPからの転記です。

ファイルアップロード時に、アップロード処理の時間が長いと
「アップロードが失敗したか、タイムアウトになった可能性があります。処理を続行しますか?」

image.png

とダイアログが出る事がありますが、
カスタマイズ時に非表示にできないか調べました。

https://github.com/netcommons/NetCommons2/blob/537e08b7a2892bacd2eed5dbac3ea45792de0f36/html/webapp/modules/common/files/js/common.js#L2451

webapp/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);
    },

ではでは。

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

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を開き、新たにリポジトリを作成していた。2

 Gitlabで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.yml
stages:
- 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 push

 Gitlab 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という文字を深い満足をもって確認する。

 スクリプトエディタをリロードすると、真っ白だったエディタの中には、先ほど作成したお気に入りのアイドルグループ表を作成するスクリプトが表示されていた。


  1. 実際には、私はGASを触ったことがないことで後ろ指を指されたことはありません。 

  2. Gitlab CIを採用したのは社内で使っているからというだけで、深い理由はありません。 

  3. 環境変数に直接入れてしまうと、悪意ある.gitlab-ci.ymlをコミットされた場合に認可情報が流出する懸念があるので、公開リポジトリにpushする場合は工夫する必要があります。 

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

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']
    });

参考

TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.

typeorm - Is it possible to 'protect' a property and exclude it from select statements - Stack Overflow

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

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列は編集させたくない)

image.png

一部のセルだけ編集可能にするには…

const columnList = [
  { title: "ID", field: "id", editable: "never" },
  { title: "name", field: "name" },
  { title: "description", field: "description" },
];

editable: "never"

をカラムの定義につけてあげると、そのカラムは編集不可になった。(ID列をクリックしても編集欄が表示されないけど、それ以外の列は編集可能)

image.png

参考になれば幸いです。

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

LaravelでFullcalendarを実装する方法

はじめに

今回はLaravelでFullcalendarを実装する方法について解説します。
LaravelでFullcalendarを実装するにあたり、参考記事があまりなく、実装に苦労したので、同じような方の助けになればと思います。

-各バージョン
-Laravel 6.x
-PHP 7.4.9
-MySQL 5.7.30
-Fullcalendar v5

Fullcalendarはバージョンによって記述方法が異なるので注意してください。
v4の記事を参考にしてもうまくいかないことが多かったです。

Fullcalendarをダウンロードする

公式ドキュメントよりダウンロードできます。
https://fullcalendar.io/docs/getting-started

NPMや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.php
public 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 EventController

indexアクションにカレンダーの表示とDBに保存されているデータをカレンダーに表示させるコードを書きます。
今回はDBファサードを利用するので、useで宣言が必要です。

EventController.php
use 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.php
Route::get('/index', EventController@index);

カレンダーの情報を登録する

カレンダーの情報を登録するためのコードを先ほど作成したEventControllerに書いていきます。データの登録後、カレンダーが表示されているページに戻るようにしています。

EventController.php
public 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.php
Route::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

これで、カレンダーにデータの登録と、登録したデータの表示ができるようになりました!

さいごに

今回は登録フォームを直接書いていますが、今後は日付から選択をしてデータの登録をしたりなど、いろいろとカスタマイズをしてみたいと思います!

最後まで読んでいただきありがとうございました!

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

【備忘録】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]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Context / useContext を書きながら学ぶ

Reactの組み込みフックであるContextuseContextの説明をします。

また、useContextは、ReactのContextと併用するので、Contextの解説もします。

Contextとは

useContextは、ContextというReactの仕組みを利用するために必要なフックです。

なので、まずはContextについて説明します。

Contextとは...と言っても様々な言い方ができるでしょう。

  • 「状態」と「状態を変更するメソッド」を、propsを用いず、アプリケーション全体で取り回すことができるやつ
  • Propsを利用せずに、様々な階層のコンポーネントに値を共有するReactのAPI

などなどですね。図で説明しましょう。

本をReactで表現するとします。

IMG_56E1E941470B-1.jpeg

ホントは、もっといろいろなコンポネントがありえますが、今回は、Bookのデータ(仮にbookDataとする)を<Page /><Title />で、使いたいとします。

IMG_1BF3BF95601E-1.jpeg

すると、このようにpropsをバケツリレーして渡す方法もあります。ですが、<Body /><Cover />はそんなデータを使用しない。とか、もっと多階層で中間のコンポネントはbookDataをまったく使わないので、バケツリレーで渡す意味がない時ありますよね?

もしくは、グローバルにどこでも取りまわせるstateを持ちたい時がありますよね?

そんな時は、Contextの出番です。

IMG_9AACCBB1127C-1.jpeg

Contextがデータストアの役割をはたし、propsを使わず直接bookDataを下層階層のコンポネントが使うことができるようにしたものです。

useContextは、Contextの機能をさらにシンプルに使うことができるやつです。

Contextを利用するために必要なもの

  • Contextオブジェクト: React.createContextというReactのAPIの戻り値
  • Provider: Contextオブジェクトが保持しているコンポネント
  • Consumer: Contextオブジェクトから値を取得しているコンポネント

サンプルコード

基本の状態は以下にします。(Bookを表すには雑すぎますが...ツリー構造さえわかればOKなので許してください)

スクリーンショット 2021-01-28 23.10.42.png

Contextを利用して、下層コンポネントに、データを受け渡します。
以下の順番で書いていきます。

  • createContext(BookContext)してexportしておく(Book コンポネントで実装)
  • Providerコンポネントを作成し、valueにオブジェクト(state)をセットします(Book コンポネントで実装)
  • 下層コンポネントで、先ほど作成したBookContextをimportします(Title コンポネントで実装)
  • importしたBookContextを使ってconsumerを作成(Title コンポネントで実装)
    • Consumerコンポネント内でbook state の中身にアクセスできます

赤枠が、変更点です?

Context___useContext_を書きながら学ぶ_-_CodeSandbox.png

実行結果はこんな感じです。

スクリーンショット 2021-01-28 23.40.37.png

propsで上の階層から渡していないのに、下階層でcreateContextしたデータが引っ張れていることがわかります。

それでは次に、useContextを使っていきましょう。

useContextとは

useContextを使った場合でも、Providerを使って値を渡す点は同じです。

構文はこんな感じです。

const Contextオブジェクトの値 = useContext(Contextオブジェクト)

Contextオブジェクトから、取得できる値は、Contextオブジェクトが保持しているProviderのvalueプロパティに指定された値です。

サンプルコード

<Cover />とその子供の<Title />は、createContextされたBookContextAuthorContextコンテキストをPrivideされているので、それぞれのデータを引っ張ることができます。このように、複数のコンテキストを扱うことも可能です。

サンプルコード
/components/Book.js
import 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.js
import 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.js
import 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つのコードの実行結果

スクリーンショット 2021-01-29 1.04.52.png

Context利用時の注意点(Context更新時に不要な再レンダーを招く)

Contextは使い方によっては、パフォーマンスの問題を引き起こす可能性があります。

なぜから、Provider内のすべてのConsumerは、Proverのvalueプロパティが更新するたびに再レンダリングするからです。

特に、以下の場合は注意です

  • 再レンダリングされる Consumer の数が多い場合
  • Consumerの子コンポネントのレンダリングコストが高い場合

不要な再レンダリングを防ぐ方法は、次の3つです。

  • Contextオブジェクトの分割
  • React.memoの利用
  • useMemoの利用

ぜひ、試しながらやってみてください。

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

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.php
Route::post('hogehoge/image/upload','Api\ProjectUploadController@upload_image');

完成

これでeditor内部にs3からのパスで画像を表示できているでしょう。
あとはquillで作成したHTML入りの文章をDBに保存するなりで大丈夫でしょう。

動かなかったら質問お願いします。
日本語の記事見かけてないので書きました。

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