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

ブラウザの戻るボタンを押せなくする(ブラウザバック禁止)

既出だとおもいますが、勉強になったのでメモがてら。

表題通り、ブラウザバックさせないようにするためには、履歴を残さないようにすればOK。
Location.replace() を使えば、ページを移動した履歴が残らないとのこと。

html 内に書くなら、onclickを使って下記通り。

<a href="javascript:void(0)" onclick="location.replace('https://xxxx')">ページをすすむ</a>

jsで書くなら下記通り。

$('.class').on('click', function () {
  window.location.replace(指定のURL);
});

■参照

http://www.htmq.com/js/location_replace.shtml
https://syncer.jp/javascript-reference/location/replace

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

inkscapeで画像を作って、jsで操作してみた。

はじめに

「webサイト上で画像を動かせるようになりたい!」
この記事を読むことで上記の望みを達成できるかと思います。
事前にinkscapeをPCにインストールしてください。インストール方法については他の記事を参照してください。

inkscapeで画像を作成

inkscapeで適当なSVG画像を作成して下さい。
今回は、以下のSVGを作成しました。
image.png

プログラム

inkspaceでSVGファイルを作成したら、htmlとjavascriptで画像を操作するプログラムを作成します。

ファイル構造↓

index.html
index.html
<!DOCTYPE html>
<html lang="en">
</summury>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <object data="./sample.svg" type="image/svg+xml" id="i1" width="400" height="400" style="display: block ;margin-right: auto; margin-left:auto;"></object>
    <button id="change_btn" style="margin-right: 50%; display: block; margin: auto;">画像が変化するよ</button>

    <script>
        let change_image = () => {
            var svgDoc = document.getElementById("i1").getSVGDocument();
            var svgtext = svgDoc.getElementById("text12");

            console.log(svgDoc);
            console.log(svgtext);
            console.log(svgtext.getAttribute("style"));
            svgtext.setAttribute("style",
            "font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect14);fill:#aa8800;fill-opacity:1;stroke:red;storoke-width:2;");
            svgDoc.getElementById("path10").setAttribute("style",
            "fill:#0000FF;fill-rule:evenodd;stroke-width:0.5");
            svgDoc.getElementById("where").setAttribute("y","-110");
        }
        document.getElementById('change_btn').addEventListener("click", change_image);

    </script>
</body>

</html>

sample.svgのコード
sample.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="210mm"
   height="297mm"
   viewBox="0 0 210 297"
   version="1.1"
   id="svg8"
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
   sodipodi:docname="test_svgs_ample.svg">
  <defs
     id="defs2">
    <rect
       x="-313.72024"
       y="-142.11905"
       width="3.0238095"
       height="18.89881"
       id="rect20" />
    <rect
       x="-452.05952"
       y="-117.17262"
       width="123.97619"
       height="58.964286"
       id="rect14" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0.0"
     inkscape:pageshadow="2"
     inkscape:zoom="0.35"
     inkscape:cx="400"
     inkscape:cy="560"
     inkscape:document-units="mm"
     inkscape:current-layer="layer1"
     inkscape:document-rotation="0"
     showgrid="false"
     inkscape:window-width="1920"
     inkscape:window-height="1000"
     inkscape:window-x="-11"
     inkscape:window-y="1609"
     inkscape:window-maximized="1" />
  <metadata
     id="metadata5">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title />
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="レイヤー 1"
     inkscape:groupmode="layer"
     id="layer1">
    <circle
       style="fill:#1a1a1a;fill-rule:evenodd;stroke-width:0.264583"
       id="path10"
       cx="105.45538"
       cy="135.69345"
       r="105.45536" />
    <text
       xml:space="preserve"
       id="text12"
       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect14);fill:#aa8800;fill-opacity:1;stroke:none;"
       transform="matrix(5.2048978,0,0,6.3519335,2395.0129,831.63321)"><tspan 
         id="where"
         x="-452.05859"
         y="-107.51641"><tspan
           style="fill:#aa8800">hello</tspan></tspan></text>
    <text
       xml:space="preserve"
       id="text18"
       style="fill:black;fill-opacity:1;line-height:1.25;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:10.58333333px;white-space:pre;shape-inside:url(#rect20);" />
  </g>
</svg>

imgタグ、CSSタグでSVGを表示すると画像を操作できない。

SVGを操作できる形で表示するには2つの方法がある。

1つ目は、SVGタグを使う方法だ。

SVGタグ内に必要なSVGファイルの中身を記述する方法だ。詳しくは「https://tiltilmitil.co.jp/blog/1494」などを参照していただきたい。

2つ目は、今回用いている手法だ。

外部のSVGファイルをobjectタグで読み込む方法だ。
<object>タグは外部からファイルを読み込むためのタグである。

<object data="./sample.svg" type="image/svg+xml" id="i1" width="400" 
height="400" style="display: block ;margin-right: auto; 
margin-left:auto;"></object>

上記のようにtype属性にtype="image/svg+xml"を指定する。

SVGのファイルを操作する。

SVGを操作しているJavaScriptの部分を以下に記載する。

 let change_image = () => {
            var svgDoc = document.getElementById("i1").getSVGDocument();
            var svgtext = svgDoc.getElementById("text12");

            console.log(svgDoc);
            console.log(svgtext);
            console.log(svgtext.getAttribute("style"));
            svgtext.setAttribute("style",
            "font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect14);fill:#aa8800;fill-opacity:1;stroke:red;storoke-width:2;");
            svgDoc.getElementById("path10").setAttribute("style",
            "fill:#0000FF;fill-rule:evenodd;stroke-width:0.5");
            svgDoc.getElementById("where").setAttribute("y","-110");
        }

ポイントは1行目。
getelementbyId()で指定して取り出した後に、getSVGDocument()をすることで、SVGファイルの中身を取り出すことができる。

取り出した要素について、style属性などを指定して、値を変更していくことで、画像の操作が可能になる。

SVG操作の様子

ボタンを押す前の状態↓
image.png

ボタンを押し、SVGを操作した後の状態↓
image.png

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

無料の天気予報APIでWEBアプリを作ってみた

初めに

vue.jsを初めて使用してアプリを作ってみました。ボタンをクリックすると各地域のリアルタイム天気が表示される予定でした。

作ったもの

長野県のボタンのみ実装された長野地域限定のWEBアプリが完成?しました。長野県の現在の様子を画像と共に教えてくれます。

使用したフレームワーク・ライブラリ

Vue.js
axios
bootstrap
OpenWeatherMap API

デモ

https://happy-fermat-76197f.netlify.app/

ディレクトリ構成図

root/
  ├ img/
  │  ├ Rain.png
  │  ├ cloudy.jpg
  │  └ sunny.jpg  
 ├ app.js
 ├ index.html
 └ style.css

ソースコード

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Nagano weather</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <!-- CSS only -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

    <!-- JS, Popper.js, and jQuery -->
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
  </head>
  <body>
    <div id="app" class="weather_box">

      <button v-on:click="upCount" type="button" class="btn btn-outline-primary">長野</button>

      <div class="weather_info">
        <ul v-if="city" id="samp">
          <li>{{ city }}</li>
          <li>{{ condition.description }}</li>   
          <li>{{ temp }}&deg;C</li>
        </ul>
      </div>
          <p v-if="condition.main == 'Rain'"><img src="img/Rain.png" alt="Rainy"></p>
          <p v-else-if="condition.main == 'Clouds'"><img src="img/cloudy.jpg" alt="Cloudy"></p>
          <p v-else-if="condition.main == 'Clear'"><img src="img/sunny.jpg" alt="Sunny"></p>
       </div>
    <div class="twitter"></div>
    <a href="https://twitter.com/Hirasawa1987?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">Follow @Hirasawa1987</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
    </div>
    <script src="app.js"></script>
    <link href="style.css" rel="stylesheet">


  </body>
</html>
style.css
ul {
    list-style: none;
  }

.weather_box {
    position: relative;
    }

  .weather_box ul {
    position: absolute;
    top: 50%;
    left: 50%;
    -ms-transform: translate(-50%,-50%);
    -webkit-transform: translate(-50%,-50%);
    transform: translate(-50%,-50%);
    margin-top: 10px;
    margin-bottom: 10px;
    padding:0;
    font-size: 60px;
    line-height: 3;/*行高は1に*/
    font-family: sans-serif;
    font-weight: bold;
    color: #000;
    -webkit-text-stroke: 1px #FFF;
    }

  .weather_box img {
    width: 100%;
    }

.twitter{
  margin-top: 10px;
}

app.js
const app = new Vue({
  el: '#app',
  data: {
    city: null,
    temp: null,
    condition: {
      main: null
    }
  },

  methods: {
    upCount: async function (event) {
      let response;
      try {
        response = await axios.get(
          'https://api.openweathermap.org/data/2.5/weather?q=Nagano,jp&units=metric&lang=ja&appid= APIkey '
        );
        this.city = response.data.name
        this.temp = response.data.main.temp
        this.condition = response.data.weather[0]
        this.iconPath = `img/${response.data.weather[0].icon}.svg`;
      } catch (error) {
        console.error(error);
      }

    },
  },
});

参考にした記事・サイト

APIを使用する際に参考にさせていただきました。
無料天気予報APIのOpenWeatherMapを使ってみる

vue.jsを書く際に参考にせていただきました。
Vue.jsを100時間勉強して分かったこと
お天気アプリで学ぶVue.jsのAPI連携

ありがとうございました。

反省点

自分の作りたかったアプリがあったので、理想と現実に劣等感を感じました。

今回は長野限定になってしまった。
とりあえず動いたこととアウトプットできたことを良しとしたいと思います。

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

コピペで使える+-トグルアイコン

自分用チートシートです。
※JQuery必須

HTML

<span class="icon"></span>

これだけ。

CSS

.icon {
    display:block;
    position:relative;
    width:30px;  //正方形推奨
    height:30px; //正方形推奨
}
.icon:before,
.icon:after{
    content:'';
    display:block;
    position:absolute;
    top:50%;
    transform:translate(0,-50%);
    width:100%;
    height:3px; //線の太さ
    background-color:pink; //線の色
}
.icon:after {
    transform:translate(0,-50%) rotate(90deg);
}

.icon.minus:before {
    opacity: 0;
}
.icon.minus {
    transform:rotate(90deg); 
}

.icon,
.icon:before {
    transition: .4s; //トグルする時のアニメーション速度
}

JQuery

$('.icon').on('click', function(){
    $(this).toggleClass('minus');
});

以上です。
スマホで情報量多めのページ作る時けっこう便利です。
大抵はトグル表示するコンテンツと合わせてdivで囲って使うと思うので、toggleClassは親divに対して行った方がいろいろとスマートにいけるかもです。

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

Laravel+Vue.jsで作成したSPAサイトでOGP対応

はじめに

個人開発でLaravel5.5とVue.jsを使用して作成したSPAサイトがあり、長らく放置していたのですが、最近勉強を兼ねて少しリファクタリングをしようかなと思いました。

ラブライブ専用掲示板

上記が作成したサイト(掲示板)になります。
治すべき部分はたくさんあるのですが、まず気になったのがTwitterカードの表示です。
特に、掲示板のスレッドURLをツイートした際に、デフォルトの情報が表示されてしまうのが見た目の部分で致命的でした。
image.png

まずやったこと

最初にapiでスレッドの情報を読み込んだ後のタイミングでquerySelectorを使用して動的にmetaタグを変更しました。

//APIでデータ取得後
document.title = this.thread_header[0].title + ' | LoveLiveBBS'; 
document.querySelector("meta[property='og:title']").setAttribute('content', this.thread_header[0].title + ' | LoveLiveBBS');
document.querySelector("meta[property='description']").setAttribute('content', this.thread_response[0]['writing']);
document.querySelector("meta[property='og:description']").setAttribute('content', this.thread_response[0]['writing']);

当たり前ですが、これでは書き換わる前のmetaが読み込まれてしまうので結果は変わりませんでした。。。

今回は/thread/[thread_id]ページのみの修正ということで、その為だけにわざわざプリレンダリングやSSRはしたくないなと思い、以下のような対応を行いました。

Laravelのbladeファイルでの対応

/thread/[thread_id]にアクセスされたときにLaravel側でデータを取得してmetaタグに設定する方法をとりました。

bladeファイルの作成

修正前は初回アクセス時にのみ使用するspa.blade.phpのみが存在していました。
スレッドページ用のthread.blade.phpを作成しました。

resources/views/threadPage.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    //コントローラーで作成したデータを表示
    <title>{{ $title }}</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="csrf-token" content="{!!csrf_token()!!}">
    <link href="https://fonts.googleapis.com/css?family=Kosugi+Maru&display=swap&subset=japanese" rel="stylesheet">
  //コントローラーで作成したデータを表示
    <meta name="description" content="{{ $description }}">
    <meta property="og:url" content="https://lovelivebbs.jp" />
  //コントローラーで作成したデータを表示
    <meta property="og:title" content="{{ $title }}" />
    <meta property="og:type" content="website">
  //コントローラーで作成したデータを表示
    <meta property="og:description" content="{{ $description }}" />
    <meta name="twitter:card" content="summary" />
    <meta name="twitter:site" content="@lovelivebbs" />
  //コントローラーで作成したデータを表示
    <meta property="og:site_name" content="{{ $title }}" />
    <meta property="og:locale" content="ja_JP" />
</head>
<body>
<div id="app">
    <app></app>
</div>

<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>

コントローラーにスレッドページ用のメソッドを追加

spa.blade.phpを返す役割だけのコントローラーにthreadPageメソッドを追加しました。

app/Http/Controllers/SpaController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Response;
use App\Thread;
use Exception;
use Illuminate\Support\Facades\Log;

class SpaController extends Controller
{
    public function index()
    {
        return view('spa');
    }

    /**
     * スレッドページのみmetaタグをbladeファイルで設定
     * @param $id
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function threadPage($id) {
        $meta = [
            'title' => 'LoveLive!BBS!',
            'description' => '当サイトはラブライブシリーズ専用掲示板です。'
        ];
        try {
            $threadDetail = Thread::where('id', $id)->first();
            if (is_null($threadDetail)) {
                return view('threadPage', $meta);
            } else {
                //取得したデータをmetaタグに設定
                $response1 = Response::where('thread_id', $id)->first();
                $meta['title'] = $threadDetail->title . ' | LoveLive!BBS!';
                $meta['description'] = $response1->writing;
                return view('threadPage', $meta);
            }
        } catch (Exception $exception) {
            Log::error('thread page exception');
            return view('spa');
        }
    }
}

DBから取得した値をまとめてbladeファイルに渡しています。

ルーティングの追加

routes/web.php
+ Route::get('/thread/{id}', 'SpaController@threadPage');

Route::get('/{any}', 'SpaController@index')->where('any', '.*');

結果

上記の変更を加えた後に、Twitterにスレッドのリンクを張ってみました。
image.png

無事、スレッドタイトルと内容が反映されました!???
こうなると次はOGPの画像生成をしたくなりますね。

あまり良いやり方ではないかもしれませんが、なんとかなりました。
もっといい方法があったら教えてください。

後このサイトですが、残念ながら全然使われてないのでラブライブが好きな方はぜひ書き込みだけでもしてみてください。。。(切実)
ラブライブ専用掲示板
ラブライブ専用掲示板:ABOUTページ

よろしくお願いします。?

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

【Vue.js】さ迷うハロオタがお誕生日カレンダーを作った

はじめに


アンジュルム元メンバー・室田瑞希

私はハロープロジェクト所属(筆頭はモーニング娘。’20ですね)のアンジュルムというグループとメンバーの室田瑞希さんという方のファンで、ここ4年くらい足繁くライブやイベントに参加していました。
昨年の誕生日イベントや地元・千葉公演など良き思い出がたくさんあります。
アンジュルム|ハロー!プロジェクト オフィシャルサイト

悲劇は起きた

アンジュルム室田瑞希が卒業を発表

2020年1月、突然の卒業発表。
当時の職場でのシビアな話合いの直後に入ってきたニュースだったので
余計にダメージを喰らいました。
その後の卒業イベントのチケットのプレミアチケット化、果てはコロナによる無観客での実施など、最後まで激動でしたね。私は今だに辛くて映像も見ておりません。
アンジュルム・室田瑞希、「ひなフェス」無観客開催で卒業「約5年間、本当にありがとうございました!」

企画

私はアンジュルムの単推しの人間なので他のグループの方のお顔と名前以外はほとんど知りません。
新しい推しを探すためにもまずは所属メンバーのことを知ることだと思いました。
新たな推しが見つかるといいな!
今回はプロフィールには必ず載っている誕生日を一覧できるハロメン誕生日カレンダーの制作を試みました。

APIを探そう

私は最近APIを叩くことに喜びを感じているので隙あらば今回も組み込んでやろうと鼻息荒くAPIを探しました。

https://api.helloproject.com/v2/artists

このようなURLで所属メンバープロフィールを取得するAPIがあるか探しました。

【結果】ありませんでした。

自分で作ろう

スクリーンショット 2020-08-05 17.42.13.png

無ければ作れば良いと思いました。
SpreadeSheetをデータベースの代わりにしてVue.jsでデータを受けとります。
現役でグループに所属しているメンバーの用情報を公式ホームページからピックアップしていきます。
ハロー!プロジェクト オフィシャルサイト
データ数は53人分なので地道に作業をしています。スクレイピングでちゃっとやりたい気持ちもありますが(できるかは別として)この作業で各メンバーのプロフィールを読んでいる時が一番楽しかったことを告白します。

構成

Untitled Diagram (1).jpg

フロントエンドのアプリケーションフレームワークにVue.jsを使用してカレンダーライブラリはFullCalendarを使用して作成しました。
Vue.jsで作成されたアプリケーションからリクエストを飛ばしてSpreadSheetのデータを取得、カレンダーに反映させます。
データの取得はこちらの記事が大変参考になりました。

Google Spreadsheet のデータを JSON 形式で取得する Web API をサクッと作る

FullCalendar
設定するためのコードを書くだけでカレンダーをブラウザに表示させることができます。
オプションも豊富です。
先輩の執筆された記事ですね、大変お世話になりました。
Vue.js×FullCallendarでWEBカレンダー作成(花粉カレンダー作成①)

moment.js
日付処理のライブラリです。
どんなプログラミング言語でも日付周りの操作は面倒なことが多いですが
moment.jsはすごく使いやすかったです。
減算加算、未来過去応用の効く使い方もできますし、かなりの好印象。

BULMA
CSSフレームワークはbulmaを選択しました。
BootStrap以外触れたことが無かったのですが
FullCalendarにより画面はほぼ決まってしまっていたため、CSSは全然触らなかったのでいずれの機会にもう一度bulmaに挑戦したいと思います。

制作物のデモ

こちらのURLで公開しております。
https://nostalgic-bhaskara-023640.netlify.app/
何度も確認しましたが間違いを見つけた同志の方がいらっしゃいましたら
教えてくださいね!


UIはFullCalendarの力によってきちんとカレンダーの役割を果たしたものになっています。
スクリーンショット 2020-08-05 19.16.55.png
イベントの色はメンバーカラーが表示されるようにしました。
これはカラフルで良いと思います。
スクリーンショット 2020-08-05 19.17.41.png

イベントをマウスオーバーするとメンバーの写真が出現します。
皆さん本当に美しいです。
いつも元気をもらっております。
コードはFullCalendarの設定が主です。
ほとんど書いてないです。

// ライブラリ関連の読み込みなど
const dayGridPlugin = window.FullCalendarDayGrid.default;
const FullCalendarInteraction = window.FullCalendarInteraction.default;
const VModal = window["vue-js-modal"].default
Vue.use(VModal);
// FullCalendar
Vue.component('calender', {
  template: '#calender',
  data: function() {
    return {
      name: "",
      imgUrl: "",
      id: "",
      calendarPlugins: [ 
        dayGridPlugin, 
        FullCalendarInteraction 
      ],
      events: [],
      // mouseOvernによるモーダルの呼び出し
      eventMouseEnter: function(event, jsEvent, view) {
        var title = event.event.title;
        app.name = title.substring(0,title.indexOf(" "))
        app.imgUrl = event.event.url
        app.id = event.event.id
        app.show()
      },
      header:{
        left:   'title',
        center: 'addPostButton',
        right:  'today prev,next'
      },
      buttonText: {
        today: '今日'
      },
    }
  },
  mounted: function(){
    this.getData();
  },
  // spreadSheetからデータを取得
  methods: {
      getData: async function (event) {
        let response;
        try {
          response = await axios.get(
            'https://script.google.com/macros/s/AKfycbwzkyHjVYKu53Hh-zS5PBGebOsT5b0kLNtnJWJ7X7s1bxcV3Me-/exec'
          );
          var this_year = moment().get('year');
          for (var i = 0; i < response.data.length; i++) {
            for (var j = 0; j < 5; j++) {
                var name = response.data[i].name;
                //  生まれ年を現在年に置換する
                var now = moment(response.data[i].date).set('year', this_year);
                // 5年後まで表示
                var target_date = moment(now, 'YYYY-MM-DD').add(j, 'year');
                var current_date = moment().add(j, 'years');
                var birth_date = moment(response.data[i].date);
                var age = this.calcAge(current_date, birth_date);
                var color = response.data[i].member_color;
                var url = response.data[i].image;
                var id = response.data[i].id;
                this.events.push({title: `${name} ${age}歳`, date: target_date.format('YYYY-MM-DD'), color: `${color}`, url: url, id: id });
             }
          }
        } catch (error) {
          console.error(error);
        }
      },
      // 年齢の計算
      calcAge: function(today, dateOfBirth) {  
        let baseAge = today.year() - dateOfBirth.year();
        let birthday = moment(
            new Date(
                today.year() + "-" + (dateOfBirth.month() + 1) + "-" + dateOfBirth.date()
            )
        );
        return baseAge;
      } 
  }
});  

let app = new Vue({
  el: '#app',
  data: function () {
    return {
        name: "",
        imgUrl: "",
        id: ""
    }
  },
  methods: {
    show : function() {
      this.$modal.show('my_modal');
    },
    hide : function () {
      this.$modal.hide('my_modal');
    },
  }
});

結論

企画にも実装にも新たな推しメン探しにも、全てにさ迷った果てに、
出来上がったものに対しての一番の感想は、
これはGoogleCalandarで十分だな、ということです。
もう少し工夫を施さないと差別化は難しいなと感じます。
制作過程はとても楽しかったですが制作物でその楽しさをなかなか表現できませんでした。

新たな推しは見つからなかった、だが・・・

見出しの通りですが新たな推しは見つかりませんでした。
50人以上のプロフィーリと写真を何度もチェックしたけど
やはり皆さん素敵でした。選べません。
ライブも再開し始めたし早く生で見たいですね!

さて、今回はWEBアプリを作成してみましたが企画段階ですごく時間がかかりました。
自分の思いついたものって大体世の中に存在することの方が多い。
obnizのように目新しいものを使えば組み合わせ次第で目を引くものは作れそうですがWEBだけで表現するのはとても難しいと感じました。

ただ今回のようにAPIの代わりとしてSpreadSheetを作ったことで解決策の新しいパターンができたこと、
制作後の結論として既存のアプリとの差別化がうまくいかなかったことなどが早いうちから分かるなど、学ぶことが多かったです。

久しぶりのWEBアプリの作成でしたが過去に何度か試みた時は環境構築を終えた時点で力尽きることばかりでした。
仮想環境を用意し、サーバーやフレームワーク、DBをインストールするなどなかなか面倒な工程をふまなければなりません。
今回の環境構築的な作業は全てCDNを使用、DBもスプレッドシートを用意するだけという、とても簡単なものでした。
限りある時間を企画→制作→発表に割き、制作物のブラッシュアップに取り組む、このサイクルはとても合理的で良い手法だと思います。
まだまだ頑張ります。

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

【JavaScript】条件(三項)演算子(if文の省略形)の使い方

【JavaScript】三項演算子(if文の省略形)の使い方

if文を省略した形で記述できる三項演算子の使い方。
名称は条件演算子、三項演算子、条件参考演算子など呼び方はまちまち。

①条件式、②trueの処理、③falseの処理の3つの値を必要とするのが名前の由来。

<構文>
条件式 ? trueの処理 : falseの処理

if文との比較

三項演算子(数値)
10 > 0 ? 'Yes' : 'No' 
if文
if(10 > 0){
       'Yes'
    }else{
       'No'
    } 

if文よりもシンプルに記述できる。

使い方の例

条件式は変数も使える

三項演算子(数値)
ok = true
ok ? 'Yes' : 'No' 

真偽値を切り替える

三項演算子(数値)
isLargeToggle: function(){
    this.isLarge == true ? this.isLarge = false : this.isLarge = true          
},
isRedToggle: function(){
    this.isRed == true ? this.isRed = false : this.isLarge = true          
}
if文
isLargeToggle: function(){
    if(this.isLarge == true){
       this.isLarge = false
    }else{
       this.isLarge = true
    }
},
isRedToggle: function(){
    console.log(this.isRed)
    if(this.isRed == true){
       this.isRed = false
    }else{
       this.isRed = true
    }
}

三項演算子を使うと短く記述できる。

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

Unchecked runtime.lastError: The message port closed before a response was received. を回避した一例

初めに

自作chrome拡張機能の制作中、とある処理をすると必ずこのエラーが出て気になっていたのですが、一行追加したら出なくなったので記念に残しておきます。

どんな処理かざっくり言いますと、
アクティブページで得た情報を、ポップアップメニューを介して拡張機能専用ウィンドウへ送信するものです。
完了したタイミングで、ポップアップ側にてエラーが出てました。

エラーを出していたコード

アクティブページ側は省略。

popup.js
const sendMessage = (id, msg) => new Promise( r => chrome.tabs.sendMessage( id, msg, s => r(s) ) );
//アクティブページの情報を取得
const res = await sendMessage( tab_id, { ... } );
//受け取ったデータを専用ウィンドウへ送信
sendMessage( window_tab_id, res );

送信後に行う処理も無いので投げっぱなし

app_window.js
chrome.runtime.onMessage.addListener( (message, sender, sendResponse) => {
    doSomething(message);
    return;
});

エラーが出なくなったコード

popup.jsは変更なしなので省略。
受信側に一行追加します。

app_window.js
chrome.runtime.onMessage.addListener( (message, sender, sendResponse) => {
    doSomething(message);
    sendResponse(); // new
    return;
});

特に何もなくても返信はしましょう、という事でしょうか。

最後に

あくまで一例です。

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

【時短テク】再生速度を指定できない動画を倍速で見るためのワンライナー

ブラウザのデベロッパーツールでこれを実行すれば動画が倍速で再生される

document.getElementsByTagName('video')[0].playbackRate = 2

注釈

  • 倍速だと早すぎる場合は、最後の = 2 を1.5に変えると1.5倍速にできます。1にすれば元に戻ります。
  • 動画(videoタグ)がページ内に一つだけの場合に使えます。 ※複数ある場合は [0] を指定したい動画にあわせればOK!

Have a nice time!

現在、多くのセミナーやカンファレンスなどのイベントが、Webinar形式&動画公開になっています。動画のインターフェースによっては、再生速度を指定できない場合も多いのですが、そんなときにこのワンライナーを使うことで、いろんなセッションを効率よく視聴することができ、さらに捗ってます!

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

[GAS][JS] 配列Aにあって配列Bにない値を取り出したい

配列Aには存在していて、配列Bに存在しないものを取り出したい。という事がありました。

ケースとしては すでに記録されているデータがあって(existData)、新たにもらったデータ(newData) のうち、existData にあるものは無視して新しいものだけを取り出したい というものでした。

教えてもらったコードが下記。きっと後でも使うのでメモ。

下記の例では [4,5] を取り出したい。

javascript/GAS
function pickupNewData(){
  const newData   = [3,4,5];
  const existData = [1,2,3];

  const filtered = newData.filter( newVal => {
    const hasValue = existData.some( existVal => existVal === newVal );
    return !hasValue;
  });
  console.log(filtered); // => [ 4, 5 ]
}

私、Javascriptで配列を扱う関数(mapとかfilterとかsomeとかreduceとか)を扱うのが苦手なんです。なのでこのコードも、filterの中でsomeがいて、パッとみて「どゆことだ?」って思ってしまうのです。ゆっくり考えたらわかるのにね。

もっといい方法がありそう?

ちなみに

return hasValue; // !を取る

にすると「どちらの配列にも存在するもの」である [3] が取得できます。

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

Google Functions: Node.jsで重要度付きのロギング

前回、Google Functions: console.infoやconsole.errorなどとログビューアの「重大度」の関係性という記事を投稿しました。そこではconsole.errorconsole.infoなどのConsole APIでは、GCP上のログの重要度(severity)は、DEFAULTERRORの二択になるということを説明しました。

GCP上の重要度はこの2つしか無いわけではなく、以下の9つのレベルがあります。

LogEntry_ _ _Cloud_Logging_ _ _Google_Cloud.png

この投稿では、Google Cloudのロギングクライアントライブラリを使って、Cloud Functionsでも重要度を指定したロギングをする方法を説明します。

この投稿で学ぶこと

  • Google Cloud Function & Node.jsで@google-cloud/loggingを使って重要度をつけたログを記録する方法
  • そして、その面倒くささ。
  • console.logと@google-cloud/loggingで記録されるログ内容の違い。

ロギングクライアントライブラリをインストールする

まず必要となるロギングクライアントライブラリをインストールします。

yarn add @google-cloud/logging

ライブラリをCloud Functionsに組み込む

次にこのロギングライブラリをCloud Functionsの実装に組み込みます。

下のコードが組み込んだものです。console.logを使ってロギングするのと打って変わって、いろいろな下準備が必要なのと、ログを記録するごとにログエントリーオブジェクトを作る必要があるのが分かります。ちょっとめんどくさそうですね。

index.js
const { Logging } = require('@google-cloud/logging')

exports.loggingWithClient = async (req, res) => {
  // クライアントを作る
  const logging = new Logging()

  // ログ出力先を決めてロガーを作る
  const log = logging.log('my-name')

  // ログエントリーを作る
  const entry = log.entry(
    {
      resource: { type: 'global' },
      severity: 'INFO', // 重要度「INFO」を指定
    },
    'ログをクライアントで書き込むテスト',
  )

  // ログを書き込む
  await log.write(entry)
  res.send('OK')
}

ひとまずこれをデプロイして、

gcloud functions deploy loggingWithClient --runtime=nodejs12 --trigger-http

呼び出してみます:

curl https://asia-northeast1-${PROJECT}.cloudfunctions.net/loggingWithClient

どのようにログが記録されたか、ログビューアを開いてみます。console.logで記録したログは、自動的にどの関数のものか関連付けされるため、管理コンソールの「Cloud Functions」から当該関数の「ログを表示」から行く導線が使えましたが、上のコードで記録したログは関数に紐づけてロギングしていないので、「ロギング」の「ログビューア」から探しに行きます:

この導線からだと、プロジェクトの全ログが出るので、たくさんログがある場合は「直近の3分」などで絞り込むと見つけやすいです。

このように、console.infoなどではできなかった重要度「INFO」でロギングされているのが確認できます:

今回試したサンプルコードでは、ログエントリーのメタデータを色々省いたため、かなり質素な内容になっています:

下は普通にconsole.logしただけのログエントリーですが、それと比べると情報の少なさが分かります:

まとめ

この投稿を通じて、次のことが分かったと思います。

  • Google Cloud Function & Node.jsで@google-cloud/loggingを使って重要度をつけたログを記録する方法
  • そして、その面倒くささ。
  • console.logと@google-cloud/loggingで記録されるログ内容の違い。

@google-cloud/loggingはかなり低レベルなロギングができる一方、使い勝手が良くなく、どの関数で実行されたかなどは自動的に記録されないので、次回はもっと利便性の高い方法について投稿したいと思います。

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

始点と終点をクリックして画像を黒塗りにする方法

http://pic2change.qcweb.jp/
というサイトをデプロイしたのですが閉めてしまうので記念に記事を書いておきます。
仕組みは簡単。画像をJSで読み込み矩形の目隠しをする、サイトです。
どうせJSなのでコードを見ようとすれば見えるので共有しておきます。

スクリーンショット 2020-08-05 16.29.21.png

document.getElementById('file').addEventListener('change', function (e){
var file = e.target.files[0];
// ファイルのブラウザ上でのURLを取得する
var blobUrl = window.URL.createObjectURL(file);
var img = new Image();
var flg = true;
var flg2 = false;
var first_x = null;
var first_y = null;
var last_x = null;
var last_y = null;
var base1 = null;
var data = null;
img.src = blobUrl;
var w =null;
var h = null;

var canvas = document.getElementById('screen_image');
if (canvas && canvas.getContext) {
  var ctx = canvas.getContext('2d');
  img.onload = function () {
    w = img.width;
    h = img.height;
    canvas.width = w;
    canvas.height = h;
    ctx.drawImage(img, 0, 0);
    canvas.addEventListener("click", first_click, false);
    canvas.addEventListener("click", fill_rect, false);

  };
//始点と終点の情報を取得、メソッドを分割しても良かった気がする
  var first_click = function (e) {
    var rect = e.target.getBoundingClientRect();
    if (flg) {
      first_x = e.clientX - rect.left;
      first_y = e.clientY - rect.top;
      flg = false;
    } else {
      last_x = e.clientX - rect.left;
      last_y = e.clientY - rect.top;
      flg = true;
      flg2 = true;
    }
  };
  document.getElementById("btn1").addEventListener("click", function(){
    var link = document.getElementById('download');
    link.href = canvas.toDataURL();
    link.download = 'download.png';
    link.click();
  });
//黒塗りする
  var fill_rect = function () {
    if (flg2 === true) {
      if (first_x > last_x) {
        [first_x, last_x] = [last_x, first_x];
      }
      if (first_y > last_y) {
        [first_y, last_y] = [last_y, first_y];
      }
      ctx.fillStyle = "rgb(0, 0, 0)";
      ctx.fillRect(first_x, first_y, last_x - first_x, last_y - first_y);
      flg2 = false;
      data = canvas.toDataURL('image/png');
      document.getElementById('screen_image').value = data;
    }
  };
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WordPress のタグ・クラウド・ウィジェットのフォント・サイズを変更する方法

問題

WordPress 標準のタグ・クラウド・ウィジェットは、フォント・サイズの指定を、以下のようにインラインで行っています。

<div class="tagcloud">
    <a href="https://example.com/tag/Wordpress" class="tag-cloud-link tag-link-13 tag-link-position-1" style="font-size: 16.4pt;" aria-label="WordPress (2個の項目)">WordPress</a>
    <a href="https://example.com/tag/PHP" class="tag-cloud-link tag-link-17 tag-link-position-2" style="font-size: 22pt;" aria-label="PHP (3個の項目)">PHP</a>
    <a href="https://example.com/tag/JavaScript" class="tag-cloud-link tag-link-19 tag-link-position-3" style="font-size: 8pt;" aria-label="JavaScript (1個の項目)">JavaScript</a>
</div>

このため CSS のみでは、フォント・サイズの変更は以下のように固定で指定することしかできません1

.tag-cloud-link {
    font-size: 16px !important;
}

解決策(PHP)

調べてみると、widget_tag_cloud_args フィルターで指定できるようである2

functions.php
add_filter(
    'widget_tag_cloud_args',
    function( $args ) {
        $args += array(
            'smallest' => 0.6,
            'default'  => 0.9,
            'largest'  => 1.2,
            'unit'     => 'rem',
        );
        return $args;
    }
);

おまけ:解決策(JavaScript)

最初、このフィルターの存在に気づかずに、JavaScript でやろうとして書いたコード。これでも意図通りに機能しますが、上記方法が使えるので、他所のサイトにユーザー・スクリプトを適用する場合ぐらいしか使い道は思いつきませんが……。

// タグ・クラウド・ウィジェットの文字サイズを調整
( () => {
    const tags = document.getElementsByClassName( 'tag-cloud-link' );
    Object.keys( tags ).forEach( key => {
        // HTMLCollection や NodeList は配列じゃないので、そのまま forEach メソッドは使えない 

        const minFontSize = 0.6;
        const maxFontSize = 1.2;
        const fontSizeUnit = 'rem';
        const defaultMinFontSize = 8; // pt
        const defaultMaxFontSize = 22; // pt

        const tag = tags[key];
        const originalFontSize = tag.style.fontSize;
        const fontSizeIncrementPercentage =
                ( parseInt( originalFontSize )  - defaultMinFontSize ) /
                ( defaultMaxFontSize - defaultMinFontSize );

        tag.style.fontSize =
            minFontSize +
            fontSizeIncrementPercentage * ( maxFontSize - minFontSize ) +
            fontSizeUnit;
    } );
} )();

参考サイト

  1. WordPressタグクラウドをカスタマイズ:フォントサイズ指定・並び替え – PIXELISTE
  2. Modify tag cloud widget font size • CSSIgniter
  3. テンプレートタグ/wp tag cloud - WordPress Codex 日本語版
  4. NodeListをforEachしたいときのパターン - yuhei blog

  1. .tag-link-size-1 みたいなクラス名をつけてくれていれば、CSS のみで対応できるのに……。 

  2. 「WordPress タグクラウド フォントサイズ」で Google 検索すると、この方法が載っている参考サイト 1 よりも上位に、wp-includes/category-template.php を編集している(ダメゼッタイ)記事や、上記のように CSS で固定しているだけの記事が引っかかってしまうので、この記事を新規に起こしました。 

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

【JS】JSを根本から再復習、スコープとかクロージャーとか

JS(VueやReact)をなんとなく使ってしまっているので、色々学習しています。
この記事は下記コースのアウトプットになります。
【JS】初級者から中級者になるためのJavaScriptメカニズム | Udemy

スコープ

  • グローバルスコープ
  • スクリプトスコープ
  • 関数スコープ
  • ブロックスコープ
  • モジュールスコープ

グローバルスコープ

windowsオブジェクト = グローバルスコープ

関数スコープ

function scope() {
  // ここに囲まれたスコープのこと指します
}

ブロックスコープ

{}で囲まれた範囲のことを指します。
varを使った場合は、ブロックスコープは生成されません。
var,let,constの違いは、ブロックスコープと巻き上げ - 30歳からのプログラミング

レキシカルスコープ(外部スコープ)

実行中のコードから見た外部のスコープのことを指します。
image.png

// 下記の場合、関数 a がレキシカルスコープになります。
function a() {
  function b() {
    console.log('だだだだだ')
  }
}

クロージャー

レキシカルスコープを関数としてしようしている状態のことをさします。

// 下記のような状態のこと
function soto() {
  let aaa = "test"
  function uchi() {
    console.log(aaa)
  }
}

プライベート変数を設定する

外部からアクセスできない変数を定義します。

function incrementFactory() {
    // 外部からアクセスできない変数
    let num = 0;
    function add() {
        num += 1;
        console.log(num);
    }
    // 関数addを返している
    return add;
}

// 変数に関数の実行を定義する
const increment = incrementFactory();

increment(); // 1
increment(); // 2

// プライベート担っているため、外部からは呼び出せない
console.log(num) // エラー

動的な関数

function addNumberFactory(num) {
    function addNumber(value) {
        return num + value;
    }
    return addNumber;
}

const add5 = addNumberFactory(5);
const add10 = addNumberFactory(10);
const result = add10(10);
console.log(result);

あとこんな書き方もできます。知らんんかった、、、

function factory(val) {
    return {
        plus: function(target) {
            const newVal = val + target;
            console.log(`${val} + ${target} = ${newVal}`);
            val = newVal;
        },
        minus: function(target) {
            const newVal = val - target;
            console.log(`${val} - ${target} = ${newVal}`);
            val = newVal;
        },
    };
}

const cal = factory(10);
cal.plus(5);
cal.minus(5);

即時関数

関数定義と同時に一度だけ実行される関数のことをさします。

実装は以下のようになります。
最初のカッコは、グループ化をさし、次のカッコは、関数の実行のことをさします。
基本的に function には名前をつけなければいけないので、()を外したらエラーになります。

// 通常の関数
function test() {
  console.log('called');
}

// 即時関数
(function() {
  console.log('called');
})()

// 変数に入れる場合は、
const c = (function() {
  let val = 10;

  function fn() {
    console.log('fn is called');
  }

  return {
    val,
    fn
  };
})()

// 関数の呼び方
c.fn();
// 変数の呼び出し方
console.log(c.val);

暗黙的な型変換

変数が呼ばれた状況によって、変数の型が自動的に変換されるので、覚えておく。

プリミティブとオブジェクト

プリミティブ

オブジェクト以外の物を全て指します。
具体的には、 String, Null, Symbolなどですね。
イミュータブルで、再代入の場合は、値の参照が切り替わります。

コピー

参照先の値がコピーされます。
元の変数を変更しても、元の変数、代入した変数は独立しているので、値は変わりません。

// address に a という変数を使って、値が読み込まれています。
let a = "Yo"
// "Yo"自体(値)がコピーされます
let b = a
// bの向き先(値)が変わります。
b = "Taka"

image.png

再代入

constを使う場合、もちろん再代入ができません。
つまり、constを定義した場合は、変数 aの向き先を変更できなくなるのです。

const a = "Yo"
a = "Taks"

オブジェクト

ミュータブルで、参照を名前付きで管理している入れ物です。
この時のミュータブルの意味は、"Yo"の値が変わったとしても、propへの参照が変わらないことから、ミュータブルと呼ばれます。

let a = {
  prop: "Yo";
}

image.png

コピー

下記図の通り、オブジェクトへの参照がコピーされることになります。(背景色が異なっているところ)
{ prop } がみている値が変更されるので、元の変数の値も変更されることになります。

let a = {
  prop: "Yo"
};

let b = a;

b.prop = "Taka";

image.png

再代入

オブジェクトの再代入はできないが、オブジェクト内のプロパティは再代入できます。

比較

オブジェクトのプロパティを比較する必要があり、オブジェクトの比較をしても、参照を比較することになるので、期待する結果とはなりません。

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

TypeScript の "?." と "!." ってなんだっけ

紛らわしい表記シリーズです。
TypeScript書いていると出てくるやつら。

?.: optional chaining
!.: Non-Null Assertion Operator

について。

?. optional chaining

TypeScript ではなく JavaScript の機能です。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Optional_chaining

接続されたオブジェクトチェーンの深くに位置するプロパティの値を、チェーン内の各参照が正しいかどうかを明示的に確認せずに読み込むことを可能にします。

nullだったらさらに先を評価しないで終わりにするためのもの。

let nestedProp = obj.first && obj.first.second;

上記の短縮表記

let nestedProp = obj.first?.second;

覚え方: nullかも?

!. Non-Null Assertion Operator

TypeScriptの機能。

https://typescript-jp.gitbook.io/deep-dive/intro/strictnullchecks

型チェッカーが結論付けられないコンテキストにおいて、そのオペランドが非nullでかつ非undefinedであることをアサートすることができます。

型チェッカーに nullable だった変数を null じゃないよって教えるためのもの。

これは単なるアサーションであり、型アサーションと同じように、あなたは値がnullでないことを確認する責任があることに注意してください。

単なるアサーションなので?.とは本質的に動きが違います。

覚え方: nullじゃない!

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

JSのfilterの使い方

参考

const words = ["Aビル","Bビル","Aさんの家","スーパー","A店"];

//const result = words.filter(word => word.length > 6);
const result = words.filter(word => word.match(/ビル/));

console.log(result);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React 関数コンポーネントでスクロールイベントを実装するには?

Reactのfunctionコードでスクロールイベント等を実装すると、
イベント関数をuseCallbackでくくってメモ化しておかないとremoveEventListenerが働かないとか、
スクロールで使用するフラグはuseRefで再レンダリングされないようにする...など、
意外と気に掛ける点が多かったので、備忘録も込めてコードを載せておきます。

import React, {
  useState, useEffect, useRef, useCallback,
} from 'react'

const TestDom = () => {
  const [isDisplay, setIsDisplay] = useState(false)

  const isRunning = useRef(false) // スクロール多発防止用フラグ

  // リスナに登録する関数
  const isScrollToggle = useCallback(() => {
    if (isRunning.current) return
    isRunning.current = true
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop
    requestAnimationFrame(() => {
      if (scrollTop > 100) {
        setIsDisplay(true)
      } else {
        setIsDisplay(false)
      }
      isRunning.current = false
    })
  }, [])

  // 登録と後始末
  useEffect(() => {
    document.addEventListener('scroll', isScrollToggle, { passive: true })
    return () => {
      document.removeEventListener('scroll', isScrollToggle, { passive: true })
    }
  }, [])

 // バツボタンでリスナ削除~ などはこのように
  const onClickClose = () => {
    document.removeEventListener('scroll', isScrollToggle, { passive: true })
    setIsDisplay(false)
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでルーレットのプログラムを作る

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

JavaScriptの自動音声再生について【Safariはいける。Chromeは対処療法。】

はじめに
年々自動音声再生は広告などの不快な影響を避けるために厳しくなっています。
今作っているアプリがJavaScriptの自動再生を必要とするものだったので色々調べてみました。

結論
Safariはいける。
Chromeは対処療法。

という感じです

試したこと
iframeタグを挿入する
https://webty.jp/staffblog/production/post-907/

JavaScriptライブラリ「Howler.js
https://dev.classmethod.jp/articles/how-to-use-javascript-library-howler-js/

結果
Safariはどちらでもできました。Chromeはどちらも不可でした。

Chromeではどうするか(対処療法)
Chromeの設定→サイトの設定→音声→URL(自分のサイト)を入力
で自動再生ができるようになりました。

なのでどうしても自動再生してほしいときはユーザーに自動再生設定を促したらいいと思います。

備考
もしChromeで自動再生できるやり方をご存知の方がいらっしゃいましたらコメ欄でお願いします!

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

kintone Promise の基礎

はじめに

kintoneをJavaScriptでカスタマイズしていると、時々意図していない動きに遭遇します。
その度に、「ああ、これは非同期処理なんだからPromiseを使って順番に処理する必要があるんだっけ」と、その度に"Promise"の書き方を確認して、デバッグしながら「何とか動いたので良しとしよう」とやってるのですが、もやもやが溜まるばかり。

今回、Promiseの基礎的な部分を自分なりに整理しておくことにしました。
「基礎であるが初歩では無い」あたりを埋めてみたいと思います。

対象者

  • JavaScript以外のプログラムの経験が結構ある
  • JavaScriptのプログラムをまあまあ書ける
  • kintoneでJSカスタマイズができる

kintone Promise とは?

まず、kintoneにPromiseが実装された背景などはこちらのリンクをご一読ください。
kintone API で Promise を使ってみよう!

次の流れで、kintone.Promiseとは
という記事がありますが、こちらを読んでも

結論:kintone.Promiseとは
な~んか処理の順番が上手くいかない時や、
な~ぜか上手く処理が反映されない時に使うと解決できるかもしれないもの。

とあって、結局もやっとしたものが残ります。

参考のコードも売上管理・在庫管理などで難しい。

Promiseは売上管理や在庫管理の処理をする際に使うものか?と思ってしまいますが、意外と単純な処理でもPromiseを使わざるを得ない場合はあります(;-)

JavaScript Promise

kintone Promiseの前に JavaScript Promise の理解が必要です。

Promiseとは何かの定義は上記(Promise)[https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise]の方が分かりやすいかと思います。

解説から引用します

Promise インターフェイスは作成時点では分からなくてもよい値へのプロキシです。
Promise を用いることで、非同期アクションの成功や失敗に対するハンドラーを関連付けることができます。
これにより、非同期メソッドは、最終的な値を返すのではなく、未来のある時点で値を持つ Promise を返すことで、同期メソッドと同じように値を返すことができるようになります。

つまり、ブラウザのイベント(例えばマウスをクリックしたとか)などの際の動作で発火するイベントリスナーの登録と同じように、Promiseを使って非同期処理の際の成功と失敗に対するハンドラーを事前に登録できると言うイメージです。

それではPromseの使い方を見ていきます。

Promise() コンストラクター

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise

Promiseを返す関数を作る

Promiseコンストラクターで関数をラップすることで、関数がPromiseを返すことができるようになります。

こんな感じ

Promiseを返す関数 promise1

const promise1 = (value) => {
    return new Promise(resolve => {
      resolve(value.toUpperCase());
    })
};
console.log(promise1()); // Promise {}

作った関数をconsole.logすると、Promise と帰ってくるので、確かにPromiseを返す関数になっているようです。

Promiseを返す関数を利用する

で、作った関数をどう使うかですが、これは下記のようにして使います。

promise1('Hello').then(result => {console.log(result)});

冗長のようですが Promise というのはこういうものと思ってください。

関数が1つだけだと説明にならないのでもう一つ追加します。

Promiseを返す関数 promise2

const promise2 = (value) => {
    return new Promise(resolve => {
      resolve(value.toLowerCase());
    })
};

Promiseを繋げる

それから、promise1 と promise2 を順番に処理させたい時は下記のように繋ぎます。

promise1('Hello')
.then(result1 => { // result1 に最初のPromise関数の結果が返ってくる
  console.log(result1); // 戻り値を使って何か処理する
  return promise2(result1) // ここで次のPromiseをリターンする
})
.then(result2 => { // 次のPromise関数の結果が返ってくる
  console.log(result2); // 戻り値を使って何か処理する
})

間に then() 関数を入れて繋げるだけです。(いわゆるPromiseチェーン)
簡単ですね。

then関数の中にコールバック関数を書くことでPromiseを返す関数を処理の順番に繋げることができます。

エラー処理

エラー処理はPromiseを返す関数の中で処理しても良いですし、作った関数を使う側で下記のようにしても良いです。

promise1('Hello')
.then(result1 => {
  console.log(result1);  
  return promise2(result1)
})
.then(result2 => {
  console.log(result1); // ここでエラーが発生
  console.log(result2);
})
.catch(error => { // ここでエラーをキャッチする
  console.log(error); // :ReferenceError: result1 is not defined
})

kintone Promise 再び

まずサンプルコードです。

サンプルコード

(function() {
  'use strict';
  kintone.events.on('app.record.detail.show', (event) => {
    return new kintone.Promise((resolve) => {
      resolve(kintone.app.getId());
    })
    .then(result => {
      const params = {'app': result};
      return kintone.api(kintone.api.url('/k/v1/app/form/fields', true), 'GET', params);
    })
    .then(result => {
      const fields = getFields(result, 'GROUP');
      fields.forEach(field => {kintone.app.record.setGroupFieldOpen(field.code, false)});
      // return event; // 処理によってはeventをリターンしなくてもOK
    })
    .catch(error => {
      console.error(error);
    });
  });

  function getFields (fields, fieldType) {
      let result = [];
      try {
          const values = Object.values(fields.properties);
          if (fieldType) {
              values.map((field) => {
                  if (field.type === fieldType) {
                      result.push(field);
                  }
              });
          } else {
              result = values;
          }
          return result;
      } catch (error) {
          return error;
      }
  }
})();

kintone.Promise CheatSheet

kintoneのPromiseの書き方については、デベロッパーサイトにもいくつかの書き方がありどれにすれば良いのか迷いますが、
まずは基本の書き方1つで行きましょう。

テンプレートを載せておきます。

  kintone.events.on('<kintoneイベント>', (event) => {
    return new kintone.Promise((resolve) => {
      // 何か処理
      resolve(<次に渡したい結果など>); // Promiseチェーンで繋げたい結果はresolve()で返却する
    })
    .then(result => {
      // 何か処理
      // REST APIでレコードを取得など。 下記の書き方をすれば、処理結果がPromiseで返される。
      return kintone.api(kintone.api.url('<REST API エンドポイント>', true), '<GET/POST>', <パラメータ>);
    })
    .then(result => {
      // 何か処理
      return event; // 結果をレコードに反映するなどの処理に応じて、最後にeventをリターンする。
    })
    .catch(error => {
      console.error(error);
    });
  });

上記のテンプレートを使ってもらえれば、下記のようにPromiseがネストしていくような書き方にならずに済むかと。。。:sweat_smile:

    .then(result => {
      return kintone.api(kintone.api.url('<REST API エンドポイント>', true), '<GET/POST>', <パラメータ>)
      .then(result => {
        return kintone.api(kintone.api.url('<REST API エンドポイント>', true), '<GET/POST>', <パラメータ>)
        .then(result => {
          return kintone.api(kintone.api.url('<REST API エンドポイント>', true), '<GET/POST>', <パラメータ>)
          .then(result => {

参考

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

ウェブサイト作成用備忘録・4号:chrome デベロッパーツールでは分からない background-image の落とし穴

日々の学習のアウトプットの為、自主学習の際に工夫した内容を記録していきます。

今回は background-image についての思い出話

自分が初めて自作のウェブページを作成し、レンタルサーバーを借りてアップロード、実機確認をした時の話…

「chromeデベロッパーツールで一通りのバグは確認した!ついに実機テストだ!」

色々調べて、やっとの思いでサイトのアップロードを完了させ、実機のPCでサイトのチェックを無事に済ませ、次に実機のスマホでサイトにアクセスしてみると…

「背景がなんかコレジャナイ…」

デベロッパーツールでは問題がなかったのに、実際のスマートフォンで確認すると、背景画像が正常に表示されない…

原因を調べた結果、以下の事が判明しました。

原因

background-size: cover; と background-repeat: no-repeat; を設定した状態で、PC用の画像サイズの大きなファイルを background-image に設定すると、デベロッパーツール上のスマートフォン表示では背景が正常に表示されるが、実機のスマートフォンでは元画像の左上部分が拡大表示されていた。

デベロッパーツール上ではPCのモニタサイズを基準にしてスマートフォンのモニタサイズを計算し、PCのモニタ上でスマホの画面表示を再現している。

しかし、デベロッパーツールと実機のスマートフォンは基準にしているモニタサイズがそもそも違う為、実際のスマートフォンよりも大幅に大きな画像や、アスペクト比の異なる画像を設定すると、スマートフォンの実機表示の際に background-size: cover; が正しく適応されなくなる為だと思われる。(※別の原因があったら逆に教えて欲しいです)

その後、PCのモニタサイズに合わせた画像で、PCとスマートフォンの両方で background-size: cover; が正しく機能する方法を探したのですが、最終的に見つけることは出来ませんでした。しかし、以下の方法で対策が可能だと判明しました。

対処法

1・メディアクエリを設定し、PC用の背景画像とスマートフォン用背景画像を複数用意する。
2・スマートフォンのモニタサイズに合わせた、画像端の繋ぎ目がループするシームレス加工された画像を使用+background-repeat: repeat; の設定

当時の自分は画像の加工があまり得意ではなかった為、対処法・2で対策しました。

※ちなみに、当たり前といえば当たり前なのですが…背景画像をスマートフォンのモニタサイズ基準にリサイズした場合、スマートフォン表示では正常に表示されますが、今度はPC表示にした際に元サイズの小さな画像が background-size: cover; でPCのモニタサイズまで引き延ばされるため、元々の画質を維持することが出来ません。

今回はこれで以上になります。

あくまで自分用の備忘録ですが、他の方の参考になれば幸いです。

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

班決めを考えるのが嫌すぎて「良い感じに班決め君」作ってみた

はじめに

コロナ前を想像してみてください。

飲み会で居酒屋にいざ着いたとき、みんながみんな
「どこの卓に行こう」
「先輩に先に席選んでもらわないと」
「新人の子はこの席のほうがいいよね」
とか考えてナッカナカ席につかない、壮絶な牽制大会が始まることありませんか?
僕は鳥貴族でよくありました。
安くてしかも旨いので大人数で行きがちなんですよね。

仕事であれば、例えばグループディスカッションをするとなったときに、どんな班分けにするかって地味に悩むと思います。
そこまで深く考えずえいやで決めてしまえばいいとは思うんですが、いざ班分けをする場面になると「この人とこの人は関わりが薄いよなあ」とか「ここ若手一人になっちゃうわ」とかいろいろ考えちゃうんですよね。

色々情報をインプットして、自動で班分けをしてくれるツールあればなあと思い、作ってみちゃいました。
アプリはデプロイして試験利用できるようにあるので是非是非利用してみてくださいね。
コードはGistをご参照ください!
App: https://dazzling-albattani-2c12ef.netlify.app/hangimekun.html
Gist: https://gist.github.com/canonno/e08ba37eff5e736aa50cd08b08b9ec01

全然関係ないですけど「班」って響きなんか小学生っぽくて懐かしいですよね

完成デモ

班の数・メンバーインプット

まず班の数を決めます。
今回は最大6人班の個数のみを設定できるようにしています。
4人班と6人班を組み合わせた場合も考えたんですが、トリッキー過ぎて断念しました。

そのあと参加者の属性をインプットします。
偉い人・先輩・新人の身分3種類と、男女の性別2種類の合計6属性の人数をインプットします。
ここでインプットすると、原則同じ属性の人がばらけるように席が配置されます
例えば新人は同じ班には固まらないとか、女性が同じ班に固まらないとか、というイメージですね。

いざ班決め

GOボタンを押すと下の図に班分けが描画されます。
コロナ収束後に飲み会の席決めにも使えるように、机っぽい配置にしてあります。
青色が男性・ピンクが女性になっています。

ロジックの中に乱数を組み込んでいるので、ボタンを押すとまた新しい配置になります。
この配置気に食わんなという場合はボタンを連打して良い感じの配置を探すという八百長も可能になっています。

もちろん、一度班分けをした後に条件変更しても大丈夫です。
一つの班の人数調整に利用できそうですね。

応用テク

例えば偉い人男性2人・先輩男性6人女性1人・新人男性2人女性1人で合計12人の時を考えます。
女性が2人しかおらず、女性2人を同じ卓に入れたいとしましょう。
こういう時は女性2人を、先輩男性6人に足して8人とみなして実行します。

すると、こんな結果になります。
image.png

この結果を受けて後出しでどこかの班の先輩男性2人を女性2人に置き換えると、必然的に女性2人を同じ班にすることができます。
ややトリッキーですが、意外といろんな用途に使えるのかなあとかぼんやり思っていたりします。
「この人たちは固める」みたいな機能をつけたかったんですが、ロジックが地味難しく途中で力尽きました。
いつかジツゲンシテェ

環境

Visual Studio Code 1.47.1

手順

いい感じのヘッダーを付ける

Progateの「HTML&CSS学習コース 初級編」というオンライン講座をかなり参考にしました。
ベイビーステップで少しずつ少しずつ、手を動かしながら勉強できるのでProgateかなりおすすめです。
オンラインですのでコロナ自粛の間是非是非お試しくださいね。
(Progate: https://prog-8.com/courses)

image.png
こんなのがサクサクつくれます

条件インプット部分作る

まずボタンをポチー押して条件がニョロンと出る部分については、簡単に見栄えのいいアイテムを追加できるBootstrapのAccordionというコンポを利用しました。
人数を選ぶラジオボタンもBootrstrapのButton Toolbarを利用しています。
型にペコペコいれるだけで見栄え良く仕上がるので是非利用してみてくださいね。

(Accordion: https://getbootstrap.com/docs/4.5/components/collapse/)
(Button Toolbar: https://getbootstrap.com/docs/4.5/components/button-group/)

<!--Bootstrapのアコーディオンを利用-->        
    <div class="accordion" id="accordionExample">
      <div class="input">
        <!--班の数指定カード-->
        <div class="input-float">
          <div class="card" style = "width:18rem;">
            <div class="card-header" id="headingOne">
              <h2 class="mb-0">
                <button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
                  <h3 class="section-title"></h3>
                </button>
              </h2>
            </div>

            <div id="collapseOne" class="collapse" aria-labelledby="headingOne" data-parent="#accordionExample">
              <div class="card-body">
                <!--4人班の数を選ぶボタン-->
                <div class = "condition-items">
                  <p>最大4人班</p>
                  <div class="btn-group btn-group-toggle" data-toggle="buttons">
                    <label class="btn btn-secondary active">
                      <input type="radio" name="yoninseki" id="option0" checked> 0
                    </label>
                    <label class="btn btn-secondary">
                      <input type="radio" name="yoninseki" id="option1" disabled> 1
                    </label>
                    <label class="btn btn-secondary">
                      <input type="radio" name="yoninseki" id="option2" disabled> 2
                    </label>
                    <label class="btn btn-secondary">
                      <input type="radio" name="yoninseki" id="option3" disabled> 3
                    </label>
                    <label class="btn btn-secondary">
                      <input type="radio" name="yoninseki" id="option4" disabled> 4
                    </label>
                  </div>
                </div>

                <!--6人班の数を選ぶボタン-->
                <div class = "condition-items">
                  <p>最大6人班</p>
                  <div class="btn-group btn-group-toggle" data-toggle="buttons">
                    <label class="btn btn-secondary active">
                      <input type="radio" name="rokuninseki" id="option0" value=0 disabled> 0
                    </label>
                    <label class="btn btn-secondary">
                      <input type="radio" name="rokuninseki" id="option1" value=1 disabled> 1
                    </label>
                    <label class="btn btn-secondary">
                      <input type="radio" name="rokuninseki" id="option2" value=2 checked> 2
                    </label>
                    <label class="btn btn-secondary">
                      <input type="radio" name="rokuninseki" id="option3" value=3> 3
                    </label> 
                    <label class="btn btn-secondary">
                      <input type="radio" name="rokuninseki" id="option4" value=4> 4
                    </label>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>

これが
image.png
これになりますベンリィ!

このまま次のAccordionを作ると、下へ下へボタンができていきます。
今回作成したように、右側にボタンを作りたい場合はcss側でfloat:leftを指定してあげましょう。
ただ指定すると今度は下へ作ってくれなくなるので、下へ追加したくなったらclear:leftを追加しましょう。
(参考:https://udemy.benesse.co.jp/development/web/css-float.html)

.input-float{
  padding: 0px 20px 0px 0px;
  float:left;
}

.contents {
  padding: 20px 20px;
  margin-top: 20px;
  clear:left;
}

クリックしたときのロジック

最初に考えたこと

GOボタンを押すと、getMemberメソッドが走って条件の読み取りと班決めを行ってくれます。
一番シンプルな班決めロジックはこれではないでしょうか。
image.png

条件から「偉男,偉男,先男,先男・・・」のようなメンバーリストを作り、左から「1班、2班、3班、4班、1班、、、」と決める手法です。
これであれば同じ属性の人はばらけそうですよね。
ただ、全て結果が同じになってしまってつまらないなあと思い、この部分に一つランダム要素を仕込んでみました。

実装したロジック

今回実装したロジックの全体像はこちらです。
大きな変更点は、一度作ったメンバーリストから、ランダムに(だけどランダム過ぎない程度に)組み替えて新しいメンバーリストを作る部分です。
image.png

このちょこっとランダム要素の具体的な処理はこちら。
全部をランダムにしてしまうと、属性で分ける意味がなくなってしまいますよね。
なので敢えて回りくどく、「先頭の4人からランダムで取り出し」⇒「新しいメンバーリストに追加」の操作を繰り返し、全体の傾向が崩れすぎない程度にランダムにシャッフルしています。
こうすることで、前半に男性がきて後半に女性がくるという全体の傾向は保持しつつ、シャッフルすることができます(たぶん)。

image.png

ジャストアイデアで実装した感じなので、実際効果あるのか検証していませんがきっとうまくいっていると信じていますスミマセンンンンン

班分けを描画する

班分けの描画にはp5jsというライブラリを使いました。
絵も描けるしマウスとインテラクティブな実装もできるし、使い勝手がめちゃめちゃ良いライブラリになっています。
是非こちらも使ってみてくださいね。
p5js: https://p5js.org/examples/

画面更新した際にはまずsetup()が実行され、描画スペースの作成と机の描画が行われます。

//画面更新の度に実行、描画範囲の新規作成
      function setup() {
        // Create the canvas
        createCanvas(920, 300);
        background(235);

        // Set colors
        fill(126, 149, 230, 127);
        stroke(0, 0, 0);
        for (i = 0;i<4;i++){
          rect(40+i*210, 70, 180, 70);
        }
      }

その後、先ほどのシャッフルしたメンバーリストを受け取り一人ずつ描画していく処理が走ります。

      //テーブルに描画していく
      function maketable(tablelist,numoftable){
        // A rectangle x座標、y座標,横,縦
        // An ellipse
        textSize(24);
        textAlign(CENTER);

        //ひたすら一人ずつ配置(テーブル左上)
        for (i=0;tablelist.length != 0 && i<numoftable ;i++){     
          gender = tablelist[0].substr(1,1);
          //「偉男」の二文字目を取り出し、男女によって色を変える
          if (gender==""){
            fill(126, 149, 230, 127);
            console.log("男や");
          }else if (gender==""){
            fill(255, 140, 255 , 127);
            console.log("女や");
          }

          //「偉男」の一文字目を取り出し描画
          mibun = tablelist[0].substr(0,1);
          ellipse(75+i*210, 40, 40, 40);
          text(mibun, 75+i*210, 50);
          tablelist.shift();
          console.log(tablelist);
        }

        //テーブル右上
        for (i=0;tablelist.length != 0 && i<numoftable ;i++){     
          gender = tablelist[0].substr(1,1);
          if (gender==""){
            fill(126, 149, 230, 127);
            console.log("男や");
          }else if (gender==""){
            fill(255, 140, 255 , 127);
            console.log("女や");
          }

・・・
・・・
・・・

あとはhtmlに仕込むだけ!

この作業が地味に難しかったんですがなんとかできました!
Bootstrap君スゲェッスマジデ
コードの全体像はGistに載せたので是非ご覧になってくださいね。
Gist: https://gist.github.com/canonno/e08ba37eff5e736aa50cd08b08b9ec01

さいごに

これで少しはストレスフリーに班決めができるのではないでしょうか!
いや個人それぞれをどこに配置するか結局決める必要あるやんとか言わないで
個人的には名前のインプット欄も作って、それを描画できたらいいなあとか思ってたんですが、人数が増えたときに全員分名前入れるの?と思い断念。
良いUI思いついた人いらっしゃいましたらコメントいただけると嬉しいです!

最後までご覧いただきありがとうございました!
Qiita毎週投稿頑張っております、LTGMいただけると励みになります!
何卒宜しくお願い致します!

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

MobileNetでチー牛診断

See the Pen チー牛 by John Doe (@04) on CodePen.

スマホの方は こちら

MobileNetで顔写真からチー牛顔かどうか判定するアプリ作りました。

チー牛とは

チーズ牛丼好きそうな顔のことです。

人工知能なら特徴量を獲得できるとのことなので、
チーズ牛丼好きそうな顔の特徴量も獲得できると考えました。

teachablemachine.withgoogle.com_train_image (3).png

TensorFlowで学習しており、精度も大変高いです。

ご自分の顔がチーズ牛丼好きそうな顔かどうか高い精度で判断できます。

ぜひ試して見えてください

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

Google Functions: console.infoやconsole.errorなどとログビューアの「重大度」の関係性

JavaScriptのConsole APIには、ロギングで良く使うconsole.log以外に、console.infoconsole.errorなど、ログに「情報」や「エラー」といった色をつけるメソッドがあります。

一方、Google Cloud Platform(GCP)のログビューアの「重大度(SEVERITY)」という概念があり、ログごとに「INFO」や「ERROR」などの意味合いを持たせることができます。

では、Console APIとGCPの「重大度」はどのような関係になっているのでしょうか? 実験してみたので、この投稿ではその結果をお伝えしたいと思います。

結論

先に結論を示します。JavaScriptのConsole APIのメソッドの違いは、基本的にGCPの重大度に影響しません。ただし、console.warnconsole.errorErrorオブジェクトをロギングした場合に限り、重大度が「ERROR」になります。

Errorオブジェクト以外をロギングした場合

Console API GCPの重大度
console.log DEFAULT
console.info DEFAULT
console.warn DEFAULT
console.error DEFAULT

Errorオブジェクトをロギングした場合

Console API GCPの重大度
console.log DEFAULT
console.info DEFAULT
console.warn ERROR
console.error ERROR

console.infoやconsole.errorなどが、ログビューアでどの「重大度」になるか検証する

各種メソッドを検証するために、次のような関数を用意しました:

index.js
exports.logging = (req, res) => {
  console.log('テキストをconsole.log')
  console.info('テキストをconsole.info')
  console.warn('テキストをconsole.warn')
  console.error('テキストをconsole.error')
  console.log(new Error('Errorオブジェクトをconsole.log'))
  console.info(new Error('Errorオブジェクトをconsole.info'))
  console.warn(new Error('Errorオブジェクトをconsole.warn'))
  console.error(new Error('Errorオブジェクトをconsole.error'))
  res.send('OK')
}

これをデプロイして、

gcloud functions deploy logging --runtime=nodejs12 --trigger-http

実行してみます:

curl https://asia-northeast1-${PROJECT}.cloudfunctions.net/logging

すると、ログビューアには次のようなログが残りました:

CleanShot 2020-08-05 at 12.10.12@2x.png

この結果を確認すると、console.warnconsole.errorErrorオブジェクトをロギングした場合は、重大度がERRORになり、それ以外はDEFAULTになったことが分かります。

次に読む

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

若干JavaScript面试问题

1.描述一下揭示模式(Revealing Module Pattern)

Module pattern的一个变种是Revealing Module Pattern。该设计模式的目的是做到很好的代码隔离,只是将需要对外公开的变量和函数暴露出来。一个直接的实现如下所示

var Exposer = (function() {
  var privateVariable = 10;

  var privateMethod = function() {
    console.log('Inside a private method!');
    privateVariable++;
  }

  var methodToExpose = function() {
    console.log('This is a method I want to expose!');
  }

  var otherMethodIWantToExpose = function() {
    privateMethod();
  }

  return {
      first: methodToExpose,
      second: otherMethodIWantToExpose
  };
})();

Exposer.first();        // Output: This is a method I want to expose!
Exposer.second();       // Output: Inside a private method!
Exposer.methodToExpose; // undefined

2.JavaScript中提升(hoisting)是什么意思?

提升(hoisting)是指JavaScript的解释器将所有的变量和函数声明都提升到该作用域的顶部,有两种提升类型:

  • 变量提升
  • 函数提升
var a = 2;
foo();                 // works because `foo()`
                         // declaration is "hoisted"

function foo() {
    a = 3;
    console.log( a );   // 3
    var a;             // declaration is "hoisted"
                         // to the top of `foo()`
}

console.log( a );   // 2

3.this关键字如何工作?请提供一些例子

在JavaScript中,this总是指向函数的“拥有者”(也就是指向该函数的对象),或则拥有该函数的对象

function foo() {
    console.log( this.bar );
}

var bar = "global";

var obj1 = {
    bar: "obj1",
    foo: foo
};

var obj2 = {
    bar: "obj2"
};

foo();          // "global"
obj1.foo();     // "obj1"
foo.call( obj2 );  // "obj2"
new foo();       // undefined

4.JavaScript中闭包是什么?请提供一个例子

闭包是一个定义在其它函数(父函数)里面的函数,它拥有对父函数里面变量的访问权。闭包拥有如下三个作用域的访问权:

  • 自身的作用域
  • 父作用域
  • 全局作用域
var globalVar = "abc";

// Parent self invoking function
(function outerFunction (outerArg) { // begin of scope outerFunction
  // Variable declared in outerFunction function scope
  var outerFuncVar = 'x';    
  // Closure self-invoking function
  (function innerFunction (innerArg) { // begin of scope innerFunction
    // variable declared in innerFunction function scope
    var innerFuncVar = "y";
    console.log(         
      "outerArg = " + outerArg + "\n" +
      "outerFuncVar = " + outerFuncVar + "\n" +
      "innerArg = " + innerArg + "\n" +
      "innerFuncVar = " + innerFuncVar + "\n" +
      "globalVar = " + globalVar);
  // end of scope innerFunction
  })(5); // Pass 5 as parameter
// end of scope outerFunction
})(7); // Pass 7 as parameter

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

Next.jsでbootstrap4の色をカスタマイズして使う

まず bootstrapsass をインストールする。

npm i bootstrap
npm i sass

ファイル構成は以下の想定で、 bootstrap-variables.scss を新しく作成する。

/
├ pages/
│ ├ _app.js
│ ├ index.js
│ └ ...
└ bootstrap-variables.scss

アプリケーション全体に適用したいので、 _app.js の先頭に記述します。

// import 'bootstrap/dist/css/bootstrap.min.css' // ← カスタマイズ不要な場合
// import 'bootstrap/scss/bootstrap.scss' // ← bootstrapをsassで読み込みたい場合
import '../bootstrap-variables.scss' // カスタマイズ用

bootstrap-variables.scss には、上書きする値を記入して、最後に bootstrap.scss をimportすればOKです。

$theme-colors: (
    "primary": #467AFF,
    "danger": #FF4646
);

@import "node_modules/bootstrap/scss/bootstrap.scss";

これでボタンの色を変更できます。

before after
image.png image.png

編集可能なパラメータは node_modules/bootstrap/scss/_variables.scss を参照のこと。

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

Google Functions & Node.js: console.logを使った最低限のロギング

この投稿では、Google Cloud Platform(GCP)のGoogle Cloud Functions(GCF)のNode.js環境で、console.logを使った最低限のロギング手法について解説します。

この投稿で学ぶこと

  • Google Cloud Functions & Node.jsでは、console.logで最低限のロギングは可能。
  • 2行以上に渡るログは行ごとに分解されるので注意。
  • jsonPayloadを意識すると、オブジェクトの構造をログに出すことも可能。

GCFではconsole.logでログを残せて、「ログビューア」で確認できる

まず、Google Cloud Functionsでどのようにロギングし、そのログをどうやって確認するのかを学びます。シンプルに1行のメッセージをconsole.logで記録してみましょう。

index.js
exports.helloWorld = (req, res) => {
  console.log('helloWorld関数を実行しています。')
  res.send('Hello World!')
}

このhelloWorld関数をGoogle Cloud Platformにデプロイします:

gcloud functions deploy helloWorld --runtime=nodejs12 --trigger-http

デプロイが完了したら、curlで関数を呼び出してみます:

curl https://asia-northeast1-${PROJECT}.cloudfunctions.net/helloWorld

どのようなログが出たかは、GCPの管理コンソールの「Cloud Functions」を開き、「helloWorld」関数のメニューの「ログを表示」を開きます。

CleanShot 2020-08-05 at 10.45.30@2x.png

開くと「ログビューア」が表示され、「クエリ結果」の部分にログの内容が表示されます:

CleanShot 2020-08-05 at 10.49.09@2x.png

ログが表示されるエリアを拡大してみると、console.logでロギングした「helloWorld関数を実行しています。」が記録されていることが分かります:

CleanShot 2020-08-05 at 10.51.40@2x.png

:bulb: 関数が実行されてからログビューアに反映されるまで、数十秒の遅延があります。なので、関数実行後すぐにログビューアを開いてもログが出ていないかもしれません。その場合は、ログが出るまでログビューアのクエリ結果にある「現在の位置に移動」ボタンをクリックして新しいログの到着を待ちましょう。

ログは>のつまみを押すと、メタ情報を見ることができます:

CleanShot 2020-08-05 at 10.57.42@2x.png

本稿では触れませんが、これらのメタ情報を活用してログを分析したりすることができます。

Node.js環境のGCFでは、console.logを使えば特にGCP側の設定をいじらなくてもログを確認できることが分かりました。

console.logで2行以上出す場合は、ログが分割されてしまうので注意

console.logは複数行の文字列をロギングすることができますが、GCPで複数行のロギングをする場合は、ログが分割されてしまうので注意が必要です。どういうことか実験して確認してみましょう。

次の関数は複数行のログを吐くものです:

index.js
exports.helloWorld = (req, res) => {
  console.log('1行目\n2行目\n3行目\n4行目\n5行目')
  res.send('Hello World!')
}

これをデプロイして呼び出してみると、ログビューアには次のようなログが残ります:

CleanShot 2020-08-05 at 11.05.04@2x.png

見ての通り、1回のconsole.logなのに、ログは5つ出来上がっています。console.logごとに1つログができると思っていると、複数行になった場合、予想外のログになるので注意しましょう。

上の例だと、ログが複数行になっても問題ないですが、困る場合もあります。例えば、下の例のようにオブジェクトをconsole.logすると、

exports.helloWorld = (req, res) => {
  console.log({
    boolean: true,
    number: 1,
    string: 'string',
    array: [1, 2, 3],
    object: {
      field1: 'aaaaaaaaaaaa',
      field2: 'aaaaaaaaaaaa',
      field3: 'aaaaaaaaaaaa',
    },
  })
  res.send('Hello World!')
}

ログがバラバラになってしまいビューアでの可読性が良くありませんし、ログをコピペするのも一手間だったりと、運用上の面倒くささが出てきます:

CleanShot 2020-08-05 at 11.14.04@2x.png

この例では再現しませんでしたが、ログ行の順番が前後してしまうケースもあったりして、オブジェクトのようなネストした構造を安心して確認できないという問題もあったりします。

オブジェクトをconsole.logするときは一旦JSONにすると、1ログになり、構造化もされる

複数行のログ、特にオブジェクトをconsole.logするときは、そのオブジェクトを一旦JSONにするといいです。JSONのログはGCPが特別扱いしてくれるので、ログが複数に分かれることが避けられ、おまけに、ログビューアでは構造化されて表示されるので見やすさも向上します。

例えば、下の関数のように、JSON.stringifyでオブジェクトをJSON化した上で、console.logするようにします:

exports.helloWorld = (req, res) => {
  console.log(
    JSON.stringify({
      boolean: true,
      number: 1,
      string: 'string',
      array: [1, 2, 3],
      object: {
        field1: 'aaaaaaaaaaaa',
        field2: 'aaaaaaaaaaaa',
        field3: 'aaaaaaaaaaaa',
      },
    }),
  )
  res.send('Hello World!')
}

この関数を実行し、そのログを確認すると1行のログにまとまっていることが分かります:

CleanShot 2020-08-05 at 11.21.59@2x.png

加えて、ログを開いてみると、jsonPayloadフィールドにオブジェクトが構造化されているのが分かります:

CleanShot 2020-08-05 at 11.23.34@2x.png

JSON.stringifyでデバッグできないオブジェクトもあるので注意

JSON.stringifyすればどんなオブジェクトもデバッグできるかというと、そうでもないので注意してください。例えば、SetMapはJSON化すると{}になってしまいます:

const map = new Map([['a', 1], ['b', 2]])
const set = new Set([1, 2, 3])

console.log(map)
//=> Map(2) { 'a' => 1, 'b' => 2 }

console.log(set)
//=> Set(3) { 1, 2, 3 }

console.log(JSON.stringify(map))
//=> {}

console.log(JSON.stringify(set))
//=> {}

こうしたJSON化時に情報が失われるオブジェクトのロギングをGCPでどうやったらいいか、そのベストプラクティスは僕も分かっていません。もし、ご存じの方がいましたら教えてください。

まとめ

  • Google Cloud Functions & Node.jsでは、特に何も設定せずともconsole.logで最低限のロギングは可能。
  • 2行以上に渡るログは行ごとに分解されるので注意。
  • jsonPayloadを意識したロギングをすれば、オブジェクトの構造をログに出すことも可能。

次に読む

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

JavaScriptからletを「絶滅」させるために足りないもの

"JavaScriptからletを絶滅させ、constのみにするためのレシピ集" という投稿を読みました。半分はネタだと思いますが、 JavaScript で const を追求すると可読性が厳しそう だなと感じました。

一方で、他の言語だと同じことにチャレンジしても、もう少しマシに書けそうに思いました。僕が普段一番よく使っている言語は Swift です。そこで、試しに Swift で同じ内容のコードを書いてみて、 JavaScript で let を「絶滅」させるために足りないもの が何かを考えてみました。

なお、 JavaScript のコードは注釈がない限り上記の投稿からの引用です。

変数・定数宣言のためのキーワード

変数 定数
JavaScript let const
Swift var let

JavaScirpt と Swift では変数・定数宣言のためのキーワードが異なります。同じ let というキーワードが JavaScript では変数宣言に、 Swift では定数宣言に用いられていてややこしいです。そのため、本投稿ではそれぞれ「 let (変数)」・「 let (定数)」として区別します。

初級

10回繰り返したいfor文

// JavaScript ( Lodash を利用)
_.range(10).forEach(i => {
  console.log(i)
})

これを Swift で書くと次のようになります。

// Swift
for i in 0 ..< 10 {
    print(i)
}

let (定数)が省略されていますが、 ilet (定数)で宣言されます。 0 ..< 10 は標準ライブラリの Range を生成します。

元記事の JavaScript のコードと同じように forEach を使って書くこともできます。しかし、 forEachbreakcontinue, return などと相性が良くないので、 for 文などの制御構文が使える場合は、制御構文を優先した方が良いと思います。

なお、 JavaScript でも for...of 文を使えば次のように書けます。

// JavaScript (※引用でない、 Lodash を利用)
for (const i of _.range(10)) {
    console.log(i);
}

元記事の想定環境は ES2017 ということですが、 for...of は ES2015 で導入されているので上記で良いように思います。ただ、僕は普段 JavaScript をバリバリ書いているわけではないので、 ES2017 前提で for...of より forEach を優先した方が良い理由があれば教えてもらえるとうれしいです。

数値配列の合計値を算出

// JavaScript( Lodash を利用)
const arr = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
const sum = _.sum(arr)
// JavaScript (reduceが分かる人向け)
const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0)

Swift の + は関数なので、次のようにして reduce+ を渡して簡潔に書けます。

// Swift
let sum = arr.reduce(0, +)

また、標準ライブラリに sum はないですが、次のように extension を書けばメソッドを追加できます。

// Swift
extension Sequence where Element: AdditiveArithmetic {
    func sum() -> Element {
        reduce(.zero, +)
    }
}
// Swift
let sum = arr.sum()

この sum メソッドは Array が準拠する Sequence に対して追加しているので、 Array の他に SetRange など任意の Sequence に対して利用できます。

// Swift
let sum = (1 ... 10).sum() // 55

なお、 Swift では標準ライブラリの型に独自のメソッドを追加しても、 JavaScript と比べると衝突の心配は小さいです。 Swift では同名のメソッドを追加した複数のライブラリを同時に利用しても、それぞれ異なるシンボル名にコンパイルされているので干渉し合うことはありません。同一ファイル上でそれらを複数 import した場合はコード上でどれを指しているか曖昧になるのでコンパイルエラーになりますが、それは別名を付けるなどして回避可能です。1

JavaScript でも prototype にメソッドを追加することはできますが、 Array などの標準の型への追加は推奨されません。 let (変数)を「絶滅」させるには、 let (変数)を書きたくなるようなケースで気軽に extension でカバーできると良いように思います。 let (変数)の利用を extension 側に閉じ込められれば、ユーザーコードから let (変数)を「絶滅」させる助けになるでしょう。

足りない機能を関数として提供することはできますが、標準ではメソッドを使うことが多いので、スタイルが混ざるのはコードを書く上でも読む上でも望ましくありません。たとえば、 ES2019 で flatMap が追加されましたが、 flatMap が関数ではなくメソッドとして提供されるのはうれしいのではないでしょうか。

JavaScript はその仕組み上、標準の型を拡張しながら衝突を避けるのは難しいので、標準で十分な道具(まさに flatMap のような)が提供されることが let の「絶滅」に役立つのではないかと思います。

オブジェクトの配列の合計値を算出

// JavaScript( Lodash を利用)
const users = [{ name: 'person1', age: 10 }, { name: 'person2', age: 20 }, { name: 'person3', age: 30 }]
const sumOfAge = _.sumBy(users, 'age')
console.log(sumOfAge)
// JavaScript (reduceが分かる人向け)
const sumOfAge = users.reduce((accumulator, currentUser) => accumulator + currentUser.age, 0)

reduce だけで書くと、 User から age を取り出すコードと、それらの合計を計算するコードが一体化して可読性が下がります。 map して ageArray に変換してから合計した方が読みやすいでしょう。しかし、その場合、 map は中間計算のためだけに Array インスタンスを生成することになり無駄です(何百万個の要素を持つ巨大な Array かもしれません)。 LazySequence を使えばそのような問題を解決できます。

// Swift
let sumOfAge = users.lazy.map(\.age).reduce(0, +)

LazySequencemap による変換は即時実行されません。上記コードでは reduce 時に遅延して要素を取り出し、要素ごとに map の変換処理が行われます。そのため、中間計算のために巨大な実体を持ったコレクションが生成されることを防げます。 JavaScript でも、これに相当する手段を標準で提供してくれれば、パフォーマンスを犠牲にせず可読性を向上させることができるでしょう。

またこの例のように、 UserArrayageArray に変換したいようなケースは多いと思います。 Swift の KeyPath\.age )はそのようなケースで役立ちます。 Lodash を使った一つ目の例では 'age' を文字列として渡して似たようなことをしていますが、 \.age は文字列ではなく静的型検査可能なのでより安全です(そもそも JavaScript は動的型付けですが)。

if文

// JavaScript
const tax = isTakeout ? 0.08 : 0.1

Swift でも同様に三項演算子が使えます。

// Swift
let tax = isTakeout ? 0.08 : 0.1

しかし、 Swift では if 文で分岐しても let (定数)が使えます。

// Swift
let tax: Double
if isTakeout {
  tax = 0.08
} else {
  tax = 0.1
}

もし else が存在しないなど、定数が網羅的に初期化されないとコンパイルエラーになります。

この例では三項演算子が適切でしょうが、これができることが後で大きな差となって利いてきます。

じゃあswitch文どうするのよ

// JavaScript
const getMessageByStatus = (status) => {
  switch (status) {
  case 200:
    return 'OK'
  case 204:
    return 'No Content'
  // ...省略
  }
}

const message = getMessageByStatus(response.status)

前述のように、 Swift では制御構文による分岐と let (定数)の初期化を組み合わせられるので次のように書けます。わざわざ switch 一つのために関数を作る必要はありません。

// Swift
let message: String
switch (response.status) {
case 200:
    message = "OK"
case 204:
    message = "No Content"
// ...省略
}
print(message)

中~上級

try-catchとの兼ね合い

元記事では、 const徹底しない JavaScript のコードは次のようになっていました。

// JavaScript
let response
try {
  response = await requestWeatherForecast() // 天気予報APIを叩く
} catch (err) {
  console.error(err)
  response = '曇り' // APIから取得できなかった場合は適当に曇りとか言ってごまかす
}
console.log(response)

しかし、 let (変数)を「絶滅」させるために、上記コードが次のように改変されていました。

// JavaScript
const response = await requestWeatherForecast().catch(err => {
  console.log(err)
  return '曇り'
})

せっかく try/catchasync/await を組み合わせられる仕組みを言語が提供しているにも関わらず、 const を使うために半分 Promise に戻ってしまいました。これはかなり辛いです。

残念ながら Swift には Swift 5.2 現在で async/await がありませんが、 Swift 6 (おそらく 2021 年リリースの次期メジャーバージョン)で導入されそうです 2 3 4 。これが使えると仮定すると、次のように書くことができます。

// Swift
let response: String
do {
    response = try await requestWeatherForecast()
} catch {
    print(error)
    response = "曇り" // APIから取得できなかった場合は適当に曇りとか言ってごまかす
}
print(response)

let (定数)を使っているにも関わらず、 JavaScript の let (変数)を使ったコードとほぼ同じになりました。ここでも制御構文と let (定数)を組み合わせて、初期化を遅延させられることが利いています。 ifswitch による分岐だけでなく、 try/catch による分岐も考慮して、定数の初期化の網羅性が判定されているわけです。

例外catchしたら早期returnしたいんだが

// JavaScript
const shouldReturn = Symbol('shouldReturn') // 普通に文字列の'shouldReturn'でも良いか?
const response = await requestWeatherForecast().catch(err => {
  console.error(err)
  return shouldReturn
})
if (response === shouldReturn) return
console.log(response)

これはさらに辛いです。 let (変数)をなくすために shouldReturn を導入するのは明らかにやりすぎでしょう(これは完全にネタでしょうが)。

これも、 let (定数)と制御構文を組み合わせられれば素直に書くことができます。

// Swift
let response: String
do {
    response = try await requestWeatherForecast() // 天気予報APIを叩く
} catch {
    print(error)
    return
}
print(response)

この場合、エラーケースは早期リターンするので、 requestWeatherForecast が成功した場合だけ response が初期化されるコードになっていても、網羅的に定数が初期化されると判断されます。

リトライ処理

「リトライ処理」について、元記事の const徹底しない コードは↓でした。

// 天気予報APIを叩く。エラーが出たら10回までリトライする
const MAX_RETRY_COUNT = 10
let retryCount = 0
let response
while(retryCount <= MAX_RETRY_COUNT) {
  try {
    response = await requestWeatherForecast() // 天気予報APIを叩く
    break
  } catch (err) {
    console.error(err)
    retryCount++
  }
}
console.log(response)

これを、 let (変数)を「絶滅」させるために、再帰関数を使って↓のように書き換えていました。

// JavaScript
// 与えられた関数をmaxRetryCount回までリトライする関数。
const retryer = (maxRetryCount, fn, retryCount = 0) => {
  if (retryCount >= maxRetryCount) return undefined

  return fn().catch(() => retryer(maxRetryCount, fn, retryCount + 1)) // retryCountを1増やして再帰呼び出し
}

const response = await retryer(MAX_RETRY_COUNT, requestWeatherForecast)

これについては、 Swift で書く場合も var を使わないのは少し難しそうです。 let (定数)と制御構文を組み合わせて定数の初期化を遅延させられるからといって、ループとの組み合わせにはループが一度も実行されないかもしれない難しさがあります。また、ループの中で初期化が必ず一度だけ行われるかをコンパイラが判断するのが困難です。 Swift コンパイラはそこまでやってくれません。

どうしても、 let (定数)で書きたいということであれば、たとえば次のように書くことはできるでしょう。

// Swift
let response: String?
do {
  response = try await (0 ..< maxRetryCount).reduce(nil) { (result, _) in
    if result != nil { return result }
    return try? await requestWeatherForecast()
  }
} catch {
  response = nil
}
print(response)

ただ、こういうケースでは可読性のために var を使って書いた方が良いです。

// Swift
var response: String?
for _ in 0 ..< maxRetryCount {
  do {
    response = try requestWeatherForecast()
    break
  } catch {}
}
print(response)

もしくは、リトライを宣言的に書けるようなライブラリ( Rx とか( Swift なら) Combine とか)を使いましょう。

番外編(不変じゃないconst)

constは再代入できないだけで、constで宣言した配列に要素を追加したり、constで宣言したオブジェクトにプロパティを追加することはできてしまいます。

これらの行為はconstという唯一神をletと同じ地位まで貶める愚行です。

// JavaScript
const arr = []
arr.push(1) // arr: [1]
const obj = {}
obj.a = 1 // obj: { a: 1 }

Swift では let (定数)を使って宣言された Array 型変数に対して変更を加えることはできません。

// Swift
let arr = []
arr.append(1) // ⛔ コンパイルエラー

Swift の Array は値型なので varlet かを変更するだけでミュータビリティをコントロールすることができます。また、 Value Semantics を持つように実装されているので、次のようなコードを書いても定数に格納された Array インスタンスが変更されることはありません。

// Swift
let a = [2, 3, 5]
var b = a
b.append(7) // a は変更されない

print(a) // [2, 3, 5]
print(b) // [2, 3, 5, 7]

配列から条件に合うものだけ抜き出す

// JavaScript
const result = arr.filter(n => n % 2 === 0)

このコードには、特に可読性に関する辛さはないと思います。

Swift でも同様に filter を使って書きます。

// Swift
let result = arr.filter { $0.isMultiple(of: 2) }

% 2 を使っても良いですが、専用のメソッド( isMultiple(of:) )があるのでそれを使った方が良いでしょう。

変数がundefinedじゃないときだけオブジェクトに追加

// JavaScript
const header = {
  'Content-Type': 'application/json',
  ...(token === undefined ? {} : { Authorization: `Bearer ${token}` })
}

これはあえて 定数にこだわるところではない気がしますが、次のように書くことはできます。

// Swift
let header = ["Content-Type": "application/json"]
    .merging(token.map { ["Authorization": "Bearer \($0)"] } ?? [:]) { _, new in new }

ただ、可読性を考えると var を使って次のように書いた方が良いでしょう。

// Swift
var header = ["Content-Type": "application/json"]
if let token = token {
    header["Authorization"] = "Bearer \($0)"
}

とはいえ、リテラルの中で分岐できる処理がほしくなることもあります。 Function Builder を使えば次のようなことができる extension を作ることも可能です5

// Swift
let header: [String: String] = .init {
    ["Content-Type": "application/json"]
    if let token = token {
        ["Authorization": "Bearer \($0)"]
    }
}

オブジェクトの値部分に処理を加える

// JavaScript ( Lodash を利用)
const obj = { a: '1', b: '2', c: '3', d: '4', /* ... */ }
_(obj).mapValues(Number).pickBy(isEven) // { b: 2, d: 4, ... }

Swift には動的なオブジェクトはないので Dictionary で書きます( isEven は Swift の標準ライブラリにないですが、 JavaScript にもないので、別途宣言されているものとします)。

// Swift
let obj = ["a": "1", "b": "2", "c": "3", "d": "4", /* ... */ ]
obj.compactMapValues(Int.init).filter { isEven($0.value) }

キーになるのは compactMapValues です。 StringInt に変換する処理は、 Int("42") ような場合は成功しますが Int("ABC") のような場合には失敗します。 Int.init は失敗したときに nil を返しますが、 compactMapValuesmapValues した上で結果が nil になるエントリーを取り除いてくれます。

JavaScript から let を「絶滅」させるために足りないもの

こうして色々なケースを比較して眺めてみると、 JavaScript から let (変数)を「絶滅」させる一番のハードルは、 const と制御構文の相性の悪さではないでしょうか。特に、 try/catchasync/awaitifswitch と組み合わせて使おうとすると途端に辛くなってしまいます。

もし JavaScript に改変を加えられるとして、この問題を解決するために僕がすぐに思いつく選択肢は次の二つです。

  1. Swift のように、制御フローを解析して網羅的に初期化されている場合は const の初期化を遅延させられるようにする。
  2. Scala や Kotlin のように、 ifswitch 等を式にする。

1 の例は↓です。

// JavaScript (※引用でない)
const a;
if (Math.random() < 0.5) {
    a = 2;
} else {
    a = 3;
}

2 の例は↓です。

// JavaScript (※引用でない)
const a = if (Math.random() < 0.5) {
    2;
} else {
    3;
}

もちろん、これくらいなら三項演算子で書けますが、 try/catchasync/await などと組み合わせてもこれができることが求められます。

他にも、上記で比較してみた範囲でも、

  • 演算子が関数でない
  • mapreduce を遅延させられない(ので中間計算のために実体を伴う無駄なコレクションを生成しないといけない)
  • 標準で足りない道具が多いわりに extension を気軽に書くのが憚られる

などが挙げられました。

逆に言えば、それらが実現されれば let (変数)の「絶滅」に一歩近づいたと言えるでしょう。

ところで、元記事には const の利点について

constなら宣言された行だけを見ればどんな値が入っているかがわかりますが、letはコード全体を追う必要があり、読み手への負担が大きいです。

と書かれています。僕は、これは必ずしも真ではないと考えています。たとえば、変数と for ループを使って 1 から 100 までの合計を求めるコードは、次のように十分小さなスコープを切れば問題ありません。変数 _sum の値を追うのが辛いということはないでしょう6

// Swift
let sum: Int // 定数
do { // 変数を使う小さなスコープ
    var _sum = 0 // 変数
    for x in 1 ... 100 {
        _sum += x
    }
    sum = _sum
}

残念ながら、 JavaScript では const の初期化を遅らせられないのでこの手を使うことはできません。 ES2017 前提で似たようなことをするなら、次のように書くことになるでしょう。

// JavaScript (※引用でない)
const sum = (() => { // 変数を使う小さなスコープ
    let sum = 0; // 変数
    for (let i = 1; i <= 100; i++) {
        sum += i;
    }
    return sum;
})();

もしくは、 let (変数)を小さなスコープに留めることを諦めるかです。その場合でも、個々の関数やメソッドが十分に小さく保たれていれば、 let (変数)が問題になることは少ないのではないでしょうか。僕個人の体験を振り返ってみても、(ローカルスコープで)定数ではなく変数を使ったことによって問題が引き起きおこされたようなケースは、もう何年も記憶にありません。

少し違った視点

ここまで、 JavaScript と Swift で変数を「絶滅」させたコードを比較し、いくつかの言語仕様や標準ライブラリの API が提供されていれば、より可読性の高いコードが書けることを見てきました。しかし、変数をより「絶滅」させやすい Swift ですが、 Swift はむしろ変数をよく使う言語です。

もちろん、 Swift でも無駄に var を使うことは推奨されませんし、 let (定数)にできるのに var になっている箇所があるとコンパイラが警告してくれます。しかし、それは var をあまり使わない方が良いということではありません。

JavaScript をはじめ、 Java, Scala, Kotlin, C#, Python, Ruby などはすべて参照型中心の言語です。 Swift がそれらの言語と決定的に異なるのは、 Swift は値型中心の言語だということです。 Array などのコレクションを含め、 Swift の標準ライブラリの型のほとんどは値型です。

値型を扱う場合、 varlet (定数)かはミュータブルかイミュータブルかということに直結します。参照型中心の言語では Value Semantics を得るためにイミュータブルクラスが広く用いられますが、値型は基本的に Value Semantics を持っているため、イミュータビリティの重要性が高くありません。

それはさらに、

  • 参照型中心 → イミュータブルな型を多用 → 式による変更が多い → 式指向が便利 → 定数で済ませやすい
  • 値型中心 → ミュータブルな型を多用 → 文による変更が多い → 文指向が便利 → 変数を活用する場面が多い

とつながります。

この関係で言えば、 JavaScript は参照型中心の言語なので式指向や定数と相性が良いはずです。しかし、言語の構文が式指向でないので上記の関係がねじれてしまって、 const を活用しづらいという見方もできるのではないでしょうか。


  1. とはいえ、他ライブラリとの名前の衝突は面倒なので、標準ライブラリなどの型の API を拡張する場合は名前衝突に配慮が必要です。ただ、ライブラリのコードではなく、アプリのコードを書いている場合はほぼ気にする必要はありません。 Swift でアプリを書くときに、それぞれの環境で都合の良い独自の extension を書くことは当たり前に行われていて、危険もほぼありません。また、仮に衝突してしまっても、別ファイルで一つずつ import して別名を付けることで、最悪ケースでも衝突を回避することが可能です。 

  2. "On the road to Swift 6" の中で concurrency が挙げられています。 

  3. "Swift Concurrency Manifesto" の Part 1 として async/await が挙げられています。 async/await についての Proposal はこちらです。 

  4. 先日 async に関する PR が Swift のリポジトリにマージされました。 

  5. ただし、 Function Builder の if let への対応は Swift 5.3 ( 2020 年秋にリリース予定の次期マイナーバージョン)からの予定です。 

  6. ただし、このコードでは定数 sum の初期化がここで完結することが構文上保証されません。その観点で言えば、式指向の言語のように、スコープの最後の式をスコープの値として直接定数に代入できるとより良いと思います。 

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

SkyWayによるビデオ・音声通話の技術概要

この記事は「マイスター・ギルド:暑中見舞!夏のアドベントカレンダー2020」3日目の記事です。

初めに...

コロナの流行が始まったとき、「Stay Home」対策で自宅に閉じ込められたとき、ITワーカーの私たちは特に自宅からリモートで仕事をすることが可能でラッキーでした。
残念ながら、私たちのほとんどはリモートでの作業に慣れておらず、最初は上司、同僚、お客さん等とリモート通信が特に困難でした。
特殊なツールを使用しても改善されましたが、同じオフィスで作業するほど自然ではありません。
弊社のMeister Guildでもその新しい作業環境に答えるツールを探して色々なツールを使ってみた:

  • Zoom:ヴァーチャル背景を使える
  • Discord:完全に無料
  • Remo:ヴァーチャルルームに入れる、共有ホワイトボードもある
  • Spatial Chat:距離によると声の高さが変わる

ビデオ会議ができるツールはほかにもあります:

各ツールが得点と弱点を持つけど「これ!」ってなるツールがなかったので「私たちの理想なビデオ会議のツール作れるかな?」と思って調査することになりました。

ビデオ会議のツール作りの調査

そのようなツールを一から開発するのはとても大変な仕事になるので、開発をスピードアップするWebRTCフレームワークを探しました。
日本製で無料プランあり、NTTコミュニケーションズが作成したWebRTCフレームワークを見つけました:image.png
ユーザー認証をテストするために、認証付きのLaravelアプリケーションを作成し、ユーザーのメールをbase64でエンコーディングしてPeerIDとして使用しました。

SkyWayとは

ホームページによるとSkyWayは:

ビデオ・音声通話の機能をアプリケーションに簡単に実装できる、
マルチプラットフォームなSDK & フルマネージドなAPIサービスです。

無料プランで下記のSDKを使える:
- Javascript SDK
- iOS SDK
- Android SDK
- WebRTC Gateway
- APIキー認証

有料プランで録音SDK管理APIも使えるんですが例えば録音も録画も普通のMedia Capture and Streams API (Media Streams)でできる。

Javascriptサンプル:

ビデオ会議

SkyWay Room example

P2Pビデオ通話

SkyWay P2P Media example

P2Pテキスト通話

SkyWay P2P Data example

録音と録画

WebRTC samples MediaRecorder

P2Pビデオ通話

通話の相手は一人です。

基本

CDNからSDKをインポートする:

headタグ内
  <script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>

カメラ映像を表示するvideo要素を追加する:

bodyタグ内
  <video id="my-video" width="400px" autoplay muted playsinline></video>

カメラ映像・マイク音声を取得する:

bodyタグ下部のscriptタグ内
  let localStream;

  // カメラ映像取得
  navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then( stream => {
    // 成功時にvideo要素にカメラ映像をセットし、再生
    const videoElm = document.getElementById('my-video')
    videoElm.srcObject = stream;
    videoElm.play();
    // 着信時に相手にカメラ映像を返せるように、グローバル変数に保存しておく
    localStream = stream;
  }).catch( error => {
    // 失敗時にはエラーログを出力
    console.error('mediaDevice.getUserMedia() error:', error);
    return;
  });

通話の相手のは「peer」と呼ばれてる。
通話出来るように自分のPeerオブジェクトを作成して相手のPeerオブジェクトと繋がる。

Peerオブジェクトの作成

Peerオブジェクトを作成するときに引数のIDを渡さない場合はランダムなIDが生成される:

scriptタグ内
        const peer = new Peer({
            key: '<SkyWayのAPIキー>',
            debug: 3
        });

PeerオブジェクトのIDはpeer.idで取得できる。

またはメールアドレスなどからIDを生成できる。
例えばLaravelのコントローラーでbase64にエンコード:

app/Http/Controllers/Controller.php
    public function index()
    {
        $user = Auth::user();
        return view('videochat',['user'=>['email'=>rtrim(base64_encode($user->email),"=")]]);
    }

ページでPeerオブジェクトに渡す:

scriptタグ内
            const peer = new Peer('{{$user['email']}}',{
                key: '<SkyWayのAPIキー>',
                debug: 3
            });

発信

相手のカメラ映像を表示するvideo要素を追加する:

bodyタグ内
  <video id="their-video" width="400px" autoplay muted playsinline></video>

相手へ発信してリスナーで接続することを待つ:

scriptタグ内
// 発信処理
const mediaConnection = peer.call('<相手のPeerID>', localStream);
setEventListener(mediaConnection);

接続ができたときにビデオ要素を設定する:

scriptタグ内
let remoteStream;
// イベントリスナを設置する関数
const setEventListener = mediaConnection => {
  mediaConnection.on('stream', stream => {
    // video要素にカメラ映像をセットして再生
    const videoElm = document.getElementById('their-video')
    videoElm.srcObject = stream;
    remoteStream = stream;
    videoElm.play();
  });
}

着信

相手側はPeerオブジェクトのcallイベントを待って着信の時ビデオ要素を設定する:

scriptタグ内
//着信処理
peer.on('call', mediaConnection => {
  mediaConnection.answer(localStream);
  setEventListener(mediaConnection);
});

映像・音声はオン・オフ等

マイク音声オフ

ミュートする:

scriptタグ内
    localStream.getAudioTracks().forEach(track => track.enabled = false);
音声オフ

相手の音声を削音する:

scriptタグ内
    remoteStream.getAudioTracks().forEach(track => track.enabled = false);

※ 音全部消したいときマイク音声もオフしなければならない。

カメラ映像オフ

カメラの映像を消す:

scriptタグ内
    localStream.getVideoTracks().forEach(track => track.enabled = false);
反響キャンセリング

反響を消す:

scriptタグ内
    localStream.getAudioTracks().forEach(track => {
        let constraints = track.getConstraints();
        constraints.echoCancellation = true;
        track.applyConstraints(constraints);
    });

ビデオ会議

ビデオ会議はビデオ通話との違いは2つ:
- 相手の数は一人以上になる
- roomオブジェクトで他のユーザーの存在(presence)が確認できる

基本

CDNからSDKをインポートする:

headタグ内
  <script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>

カメラ映像を表示するvideo要素を追加する:

bodyタグ内
  <video id="js-local-stream"></video>

カメラ映像・マイク音声を取得する:

bodyタグ下部のscriptタグ内
  // カメラ映像取得
const localStream = await navigator.mediaDevices
    .getUserMedia({
      audio: true,
      video: true,
    })
    .catch(console.error);

  // Render local stream
  const localVideo = document.getElementById('js-local-stream');
  localVideo.muted = true;
  localVideo.srcObject = localStream;
  localVideo.playsInline = true;
  await localVideo.play().catch(console.error);

相手達のカメラ映像を表示する要素を追加する:

bodyタグ内
    <div class="remote-streams" id="js-remote-streams"></div>

Peerオブジェクトの作成

PeerオブジェクトのIDを生成する。
例えばLaravelのコントローラーでbase64にエンコード:

app/Http/Controllers/Controller.php
    public function index()
    {
        $user = Auth::user();
        return view('videochat',['user'=>['email'=>rtrim(base64_encode($user->email),"=")]]);
    }

ページでPeerオブジェクトに渡す:

scriptタグ内
            const peer = new Peer('{{$user['email']}}',{
                key: '<SkyWayのAPIキー>',
                debug: 3
            });

roomオブジェクト

Peerオブジェクトが生成された後でroomに参加する:

scriptタグ内
  peer.on('open', () => {
    const room = peer.joinRoom('test', {
      mode: 'sfu',
      stream: localStream,
    });
  });

※ roomは2つのタイプがある:'sfu'(通信がサーバーを通す)と'mesh'(通信が直接にPeerへ発信する)。

roomのイベント

open

roomに入ったとき:

scriptタグ内
    room.once('open', () => {
      ...
    });
close

roomを出たとき:

scriptタグ内
    room.once('close', () => {
      // テキスト通信を止める
      sendTrigger.removeEventListener('click', onClickSend);
      // 相手達のビデオストリームを止める
      Array.from(remoteVideos.children).forEach(remoteVideo => {
        remoteVideo.srcObject.getTracks().forEach(track => track.stop());
        remoteVideo.srcObject = null;
        remoteVideo.remove();
      });
    });
open

一人がroomに入ったとき:

scriptタグ内
    room.on('peerJoin', peerId => {
      ...
    });
open

一人がroomを出たとき:

scriptタグ内
    room.on('peerLeave', peerId => {
      // ストリームを閉じてvideo要素を消す
      const remoteVideo = remoteVideos.querySelector(
        `[data-peer-id=${peerId}]`
      );
      remoteVideo.srcObject.getTracks().forEach(track => track.stop());
      remoteVideo.srcObject = null;
      remoteVideo.remove();
    });
stream

roomに入った一人のストリームを表示:

scriptタグ内
    room.on('stream', async stream => {
      // video要素を生成
      const newVideo = document.createElement('video');
      newVideo.srcObject = stream;
      newVideo.playsInline = true;
      // peerLeaveイベントのときにストリームを見つけるためにpeerIdを付ける
      newVideo.setAttribute('data-peer-id', stream.peerId);
      remoteVideos.append(newVideo);
      await newVideo.play().catch(console.error);
    });

data

メッセージを着信したとき:

scriptタグ内
    room.on('data', ({ data, src }) => {
      // メッセージと発信者を表示
      messages.textContent += `${src}: ${data}\n`;
    });

テキスト発信

メッセージを発信するとき:

scriptタグ内
    sendTrigger.addEventListener('click', onClickSend);

    function onClickSend() {
      // websocketでroomの皆さんにメッセージを起こる
      room.send(localText.value);
      // メッセージと発信者を表示
      messages.textContent += `${peer.id}: ${localText.value}\n`;
      // インプットを消す
      localText.value = '';
    }
  });

映像・音声はオン・オフ等

マイク音声オフ

ミュートする:

scriptタグ内
    localStream.getAudioTracks().forEach(track => track.enabled = !audioInStatus);
音声オフ

相手の音声を削音する:

scriptタグ内
    remoteVideos.forEach(video => {
      video.srcObject.getAudioTracks().forEach(track => track.enabled = !audioOutStatus);
    });

※ 音全部消したいときマイク音声もオフしなければならない。

カメラ映像オフ

カメラの映像を消す:

scriptタグ内
    localStream.getVideoTracks().forEach(track => track.enabled = false);
反響キャンセリング

反響を消す:

scriptタグ内
    localStream.getAudioTracks().forEach(track => {
        let constraints = track.getConstraints();
        constraints.echoCancellation = true;
        track.applyConstraints(constraints);
    });

音声通話

navigator.mediaDevices.getUserMedia()の引数でメディアのタイプ(画像・音声・両方)等を選択できる:

scriptタグ内
  // カメラ映像取得
const localStream = await navigator.mediaDevices
    .getUserMedia({
      audio: true,
      video: false,
    })

画面共有

自分の画面をMediaStreamとして取得できる

scriptタグ内
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });

このメディアストリームを使って画面共有機能が実現できる。

終わりに...

WebRTCを使用できるのでウェブアプリケーションの元に使用できるフレームワークと思いました。

その調査をして私の理想なリモートワークのツールについていっぱいなアイデアが生まれて社長が本気で開発始めようのは本気になって欲しいです。

参考

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

あなたにピッタリなジェイソン・ステイサムの映画を教えてくれるアプリを作りました【Vue.js】

はじめに

こんにちは。ジェイソン・ステイサムが大好きなたわちゃんと申します。
(参照:「ジェイソン・ステイサムで妄想するのが日課になっていたので、いっそBOTにしてみた。」https://qiita.com/twtjudy1128/items/88f3e8f09c449f49456c

今回も愛情が溢れすぎた結果、ステイサム関連のアプリを作成してしまいました。
ステイサムが好きな方も、そうでない方も、ぜひ一度お試しいただけますと幸いです。

目的

もっとステイサムの魅力を伝えたい
ステイサムの映画をみんなにも見てほしい
なんならステイサムと結婚したい
・Vue.jsを使ったフロントエンドの実装に挑戦する

完成したもの

▼コチラが実際に作ったものです!
https://trusting-yalow-80ebbf.netlify.app/jason.html

2枚の画像から、気になるステイサムたんの画像を選んでいきます。
それを10回繰り返すと・・・

最終的に選んだ画像を元に、あなたにオススメのステイサムたんの映画を教えてくれます!より魅力が伝わるように、YoutubeからTrailerを埋め込んでます♪

作り方

▼ソースコードの全貌はGistから
https://gist.github.com/twtjudy1128/86f9adb21072c6ffc5f79bb28049872f

フレームワーク・ライブラリなど

・Vue.js
・Youtube Player API
・Bootstrap

画像の切り替え部分

Bookstrapのcardを使って、まず枠組みを作ります!

      <div class="card-group">

      <!-- A card -->
      <div class="card bg-dark" style="width: 30rem;" v-show="show">
        <img v-bind:src="a.imgsrc" class="card-img-top" alt="img-thumbnail" />
        <div class="card-body">
          <button class="btn btn-lg btn-block btn-danger" v-on:click="clickA()" ></button>
        </div>
      </div>

      <!-- B card -->
      <div class="card bg-dark" style="width: 30rem;" v-show="show">
        <img v-bind:src="b.imgsrc" class="card-img-top" alt="img-thumbnail" />
        <div class="card-body">
          <button class="btn btn-lg btn-block btn-danger" v-on:click="clickB()" ></button>
        </div>
      </div>

script部分に全画像分の画像URL、動画URL、紹介文をまとめて、配列がシャッフルされるようにします!クリックされるごとに、crickCountが増えていくので、その数字と同じ配列番号の画像が出てくるようにすることで、重複しないようにしました!

   <script>
      let clickCount = 0;
      const app = new Vue({
        el: '#app',
        data: {
          show: true,
          show2: false,
          message : '',
          a: {
            imgsrc: '',
            movie:'',
            resultText:''
          },
          b: {
            imgsrc: '',
            movie:'',
            resultText:''
          },
          imgs: [
            {//メカニック
              imgsrc: 'https://cdn-ak.f.st-hatena.com/images/fotolife/u/unias_tawa/20200804/20200804083430.jpg',
              movie: "https://www.youtube.com/embed/CMklQNn0OH0",
              resultText: "あなたにオススメな映画は「メカニック」何事も完璧なスマートなステイサムたんが見れるよ!"
            },
            //(省略しますが、ここに全画像分の情報を入れます)
          ],
        },
        async created() {
          // 配列をシャッフル
          this.imgs.sort(() => Math.random() - 0.5);
          // 最初の画像もシャッフルした0と1番目
          this.a.imgsrc = this.imgs[0].imgsrc;
          this.a.movie = this.imgs[0].movie;
          this.a.resultText =this.imgs[0].resultText;
          this.b.imgsrc = this.imgs[1].imgsrc;
          this.b.movie = this.imgs[1].movie;
          this.b.resultText =this.imgs[1].resultText;
          // 次に出てくる画像の配列番号
          clickCount = 2;
        },
        methods: {
          //Aボタンを押したら、Bの画像を次の画像に切り替え
          clickA: function () {
            console.log('入れ替える配列番号:' + clickCount);
            //ランダムで画像変更
            this.b.imgsrc = this.imgs[clickCount].imgsrc;
            this.b.movie = this.imgs[clickCount].movie;
            this.b.resultText =this.imgs[clickCount].resultText; 
            clickCount++;                  
          },
          //Bボタンを押したら、Aの画像を次の画像に切り替え
          clickB: function () {
            console.log('入れ替える配列番号:' + clickCount);
            //ランダムで画像変更
            this.a.imgsrc = this.imgs[clickCount].imgsrc;
            this.a.movie = this.imgs[clickCount].movie;
            this.a.resultText =this.imgs[clickCount].resultText;
            clickCount++;
          }
        }
      });

結果画面への切り替え

まず、divにshow2のクラスをつけて、初期値をfalseにして非表示にしておきます。(前述のコード参照)

      <div class="result" v-show="show2">
         <!-- Youtube -->
         <iframe width="640" height="360" v-bind:src="movie"
         frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

        <h4><br>{{message}}</h4>
      </div>

そして、scriptでclickCountが11回以上になったら、画像画面を非表示にし、結果画面が表示されるようにしました!

        methods: {
            clickA: function () {
               if(clickCount > 11){
                  this.show = !this.show;
                  this.show2 = true;
                  this.movie = this.a.movie;
                  this.message = this.a.resultText;
                }
            },
            clickB: function () {
              if(clickCount  > 11){
                 this.show = !this.show;
                 this.show2 = true;
                 this.movie = this.b.movie;
                 this.message = this.b.resultText;
              }
            }
          }

作ってみた感想・反省点

こうやって見ると比較的シンプルですが、初心者の私は制作に15時間以上かかりました。とても苦しかったですが、当初に思い描いていたものが作れた喜びは大きいです。

そして、何よりステイサムがかっこいい。

彼の画像や動画があったからこそ、15時間も向き合えたのだと思います。
もうほんと好き。大好き。

HTMLとCSSも今回初めてまともに触れたのですが、フロントエンド楽しいですね。
それがわかったのも嬉しいなぁって思いました。

反省点は、スマホ最適化ができなかったことと、できればポップアップみたいな感じで動画を出したかったことです。プラスアルファの実装ができるように、早く基礎を積みたいです!

おわりに

是非アプリ使ってみて、どの映画をオススメされたか、コメントやTwitterなどで教えてください♪
また、こうしたらもっと良いよ!といったアドバイスもいただけると嬉しいです。
<< アプリURL▶https://trusting-yalow-80ebbf.netlify.app/jason.html >>

本業は営業企画なので、仕事と両立して勉強するの正直しんどいんですけど、毎週のQiita投稿が本当に励みになってます!

最後までご覧いただき、ありがとうございました~~~!

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