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

リングフィットアドベンチャーのようにJoyConを用いてGoogleStreetViewの世界を歩き回る【Unity+GoogleMapsJavaScriptAPI】

リングコンは使いません(レッグバンドとジョイコンだけ)

経緯

  • スタンディングデスク+ステッパーという意識高そうな在宅ワーク環境を手に入れた...のはいいものの、すぐ飽きた。座りたい。
  • 運動の動機付けとしてサイクリングマシンでストリートビューを操作するやつ(これとか)をステッパーで作ってみようと思った。
  • 運動を検知するためにセンサーが必要。何かしらの動きをマイコンで検知するとして、さらにそれを何かしらの手段でPCに送信して、...結構大変だなあ。お金もかかりそうだ。
  • どっかに運動(特に傾き)を検知できてBluetoothでPCに接続できる機械ないかなあ~~~~~。

36661.png

  • あった~~~~~~!!!!。ジョイコンで傾き検知が可能。ついでにレッグバンド(リングフィットアドベンチャーの付属品)でちょうど足につけられる。(レッグバンドはなくてもどうにでもなります)

というわけでジョイコンを用いてストリートビューの世界を歩き回ろうと思います。

方針

joyコンとの連携のためにライブラリが充実しているUnityを使用することは決定。ただしストリートビューを利用するためにはブラウザからHTMLファイルとして開く必要があるらしい。よって、Unityからブラウザを開く+開いたページをSeleniumで操作するという方針に決定。

以下の二つから構成される。

  • 表示するページ(HTML+javascript)
    • GoogleMapsJavaScriptAPIを用いてグーグルストリートビューの表示を行う。
    • 「ボタンを押すと前進する」という機能だけ持つ。
  • Unity側
    • selenium+chromeDriverを用いてページを開く。
    • ジョイコンの傾きを検知し、歩数をカウントする。歩くたびにwebページのボタンを押す。

表示するページ

参考サイト:

GoogleStreetViewが勝手に動く!?GoogleMapAPIを使った自動再生プログラムを作ってみる。|株式会社アーティス
https://www.asobou.co.jp/blog/web/streetview

まずAPIキーを取得します。このページ等を参考にしてGoogleMapsJavaScriptAPIの有効化とキーの取得を行ってください。googleのAPIには無料枠があり、月額200ドル分まで(およそ14000回ほど)無料で使えるようです。

表示するページは以下のように作成します。これはほとんど参考サイトで書かれたコードのままですが、ボタンを追加してそれを押すたびに(5回に1回)移動する、というように書き換えました。また、この記事を参考に多少座標がずれても修正できるようにしています。

<html>
<head>
    <title>StreetView</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <input type="button" onclick="proceed()" id="count" value="0歩">
    <style>
        html,
        body {
            height: 100%;
            margin: 0;
            padding: 0;
        }

        #street-view {
            height: 100%;
        }
    </style>
</head>
<body>

<div id="street-view"></div>


<script>
    const LIMIT = 300; // 移動回数の上限値
    const START_LAT_LNG = {lat: 任意の緯度, lng: 任意の経度}; // 開始地点の緯度、経度
    //const START_LAT_LNG = {lat: 35.1010033, lng: 139.0779688}; // 例
    const START_HEADING = 180; // 開始時の方角
    let panorama;
    isProceed = false;
    let count = 0;
    let m = 0;
    let n = 5; // 何歩ごとに1進むか。Unityでも設定する。少ない方が嬉しいが無料枠に注意
    let buttonText = document.getElementById('count');
    function initMap() {
        let Links, count = 0, timer_id;
         panorama = new google.maps.StreetViewPanorama(
            document.getElementById('street-view'), {
                position: START_LAT_LNG,
                pov: {
                    heading: START_HEADING,
                    pitch: 0
                },
                zoom: 1
            }); // 
        // 座標の微妙なズレに補正を加える
        svs = new google.maps.StreetViewService();
        svs.getPanoramaByLocation(START_LAT_LNG, 50, function(result, status) {
            if (status == google.maps.StreetViewStatus.OK){
                    panorama.setPosition(result.location.latLng);
            }
        });
        Links = panorama.getLinks();



    }

    function proceed()
    {
        m++;
        if(m >= n)
        {
            m = 0;
            if (panorama.getStatus() == "OK") {
                Links = panorama.getLinks();
                if (count > LIMIT) {
                    alert('stop');
                    return false;
                }
                let target = 0;
                if (Links.length >= 4) {
                    target = Math.floor(Math.random() * Links.length);
                } else {
                    let val = 360;
                    let currentPov = panorama.getPov();
                    Links.forEach(function (element, index) {
                        let ans = Math.abs(currentPov.heading - element.heading);
                        if (val > ans) {
                            val = ans;
                            target = index;
                        }
                    });
                }
                panorama.setPov({
                    heading: Links[target].heading,
                    pitch: 0
                });
                panorama.setPano(Links[target]['pano']);

            };            
        }

        count++;
        buttonText.value = String(count) + "";
    }
</script>

<script src="https://maps.googleapis.com/maps/api/js?key=取得したキー&callback=initMap"
        async defer></script>
</body>
</html>

Unity側の設定

ジョイコン検知のための設定

以下のサイトを参考にしました。

【Unity】Nintendo Switch の Joy-Con を使用する方法

【Unity】Nintendo Switch の Joy-Con のジャイロ・加速度・傾きの値を取得したり、振動させたりすることができる「JoyconLib」紹介

上の内容に従いProject Settingsから入力の変更、二つのライブラリの導入を行いました。

selenium+chromedriverの導入

参考サイト: UnityのC#からSelenium経由でChromeをゴニョゴニョする

上の内容に従い、SeleniumとChromeDriverの導入を行いました。ChromeDriverはバージョンに注意してください。

コード

Start()時にChromeDriverを用いてブラウザを起動。歩行を感知するたびにボタンを押す。

歩数の取得をどうやるかは一考の余地がありますが、とりあえず以下のように設定しました。

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;


public class JoyConWalker : MonoBehaviour
{
    private ChromeDriver _driver;
    private List<Joycon>    m_joycons;
    private Joycon          m_joyconL;
    private Joycon          m_joyconR;
    private Joycon.Button?  m_pressedButtonL;
    private Joycon.Button?  m_pressedButtonR;
    int count = 0;
    float LlastUpper = 0;
    float LlastLower = 0;
    float Llast = 0;
    bool LisUp = true;
    float theta = 0.08f; //閾値。設定は微妙なので要調整。
    void Start()
    {
        var path = Application.streamingAssetsPath;
        _driver = new ChromeDriver(path);

        _driver.Navigate().GoToUrl(Application.dataPath + "/StreetView.html");

        m_joycons = JoyconManager.Instance.j;
        if ( m_joycons == null || m_joycons.Count <= 0 ) return;
        m_joyconL = m_joycons.Find( c =>  c.isLeft );
        m_joyconR = m_joycons.Find( c => !c.isLeft );
    }

    void Update()
    {
        var orientationL = m_joyconL.GetVector();
        var current = orientationL.z;
        if(LisUp)
        {
            if(current >= Llast)
            {
                LlastUpper = current;
            }
            else
            {
                if(LlastUpper - LlastLower >= theta)
                {
                    Proceed();
                }
                LisUp = false;
                LlastLower = current;
            }
        }
        else
        {
            if(current <= Llast)
            {
                LlastLower = current;
            }
            else
            {
                if(LlastUpper - LlastLower >= theta)
                {
                    Proceed();
                }
                LisUp = true;
                LlastUpper = current;
            }            
        }
        Llast = current;


    }

    void Proceed()
    {
        count++;
        Debug.Log(count);
        _driver.FindElement(By.Id("count")).Click();

    }

    void OnDestroy()
    {
        _driver.Dispose();
    }
}

Unityで組み立てる

シーンに空のゲームオブジェクトを配置して↑のcsファイルと、JoyConManager.csをつける。htmlファイルをAssets下に配置。

これで完成。起動し、ページが開いたら左のジョイコンをレッグバンドに入れて足踏みをします。

座標は自分の好きな観光地である熱海に設定。歩数表示が左上に出ています。ステッパーで足踏みしてる間、延々と熱海をさまようことができます。

rapture_20201125221939.jpg
Asset下の構成はgitにアップロードしておきました。

https://github.com/NNNiNiNNN/JoyCon_StreetView

課題

初期座標の決定が難しいこと

現在は完全に固定した座標からスタートして、(アルゴリズム的に)完全に固定されたルートを通るようになっています。座標の決定を完全にランダムにしてから最寄りのデータを持つ座標に補正する、というやり方も試してみましたが、この手段で得られるパノラマ座標は「ストリートビューとして移動できるパノラマ座標」に限らないため、移動先を失うパターンが多くありました。apiも他にどのようなものが使えるのか把握していないため、自分がすぐ直すのは難しそうです。理想を言えば初期地点と目的地点を手動で決定して自動でルート案内するのがいいと思います。実装も多分可能ですが、今回はなしで。

歩行の検知

単純にジョイコンの傾きの差を取ればいいだけ、だとは思うのですがいまいち最適化できていません。そもそもxyzのどれを取ればいいのかもはっきり決め切れていません。現在は動きの閾値を小さめに設定することでだいたいの動きを無理やり取る形にしています。

ジョイコンの接続エラー?

一度接続したジョイコンは、その接続が途切れた後はPCを再起動しないと再接続できなくなることがあります。理由も再現性もよくわかっていません。

あとがき

課題は多くありますが、よく見るフィットネスバイク+ストリートビュー....の亜種を自力で作れたことが嬉しいです。ジョイコンとかgoogleのAPIとか、見渡せば色々便利なものがあるんですねえ。苦戦した部分もありましたが、最終的には簡単な内容で仕上がったのもよかったです。

作ったもので運動するかはわかりません。

参考

GoogleStreetViewが勝手に動く!?GoogleMapAPIを使った自動再生プログラムを作ってみる。|株式会社アーティス

Googleマップストリートビューで目的地に最寄りのストリート位置(座標情報)を取得する方法

【Unity】Nintendo Switch の Joy-Con を使用する方法

【Unity】Nintendo Switch の Joy-Con のジャイロ・加速度・傾きの値を取得したり、振動させたりすることができる「JoyconLib」紹介

UnityのC#からSelenium経由でChromeをゴニョゴニョする

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

リングフィットアドベンチャーの道具でグーグルストリートビューを歩き回る【Unity+GoogleMapsJavaScriptAPI】

リングコンは使いません(レッグバンドとジョイコンだけ)

経緯

  • スタンディングデスク+ステッパーという意識高そうな在宅ワーク環境を手に入れた...のはいいものの、すぐ飽きた。座りたい。
  • 運動の動機付けとしてサイクリングマシンでストリートビューを操作するやつ(これとか)をステッパーで作ってみようと思った。
  • 運動を検知するためにセンサーが必要。何かしらの動きをマイコンで検知するとして、さらにそれを何かしらの手段でPCに送信して、...結構大変だなあ。お金もかかりそうだ。
  • どっかに運動(特に傾き)を検知できてBluetoothでPCに接続できる機械ないかなあ~~~~~。

36661.png

  • あった~~~~~~!!!!。ジョイコンで傾き検知が可能。ついでにレッグバンド(リングフィットアドベンチャーの付属品)でちょうど足につけられる。(レッグバンドはなくてもどうにでもなります)

というわけでジョイコンを用いてストリートビューの世界を歩き回ろうと思います。

方針

joyコンとの連携のためにライブラリが充実しているUnityを使用することは決定。ただしストリートビューを利用するためにはブラウザからHTMLファイルとして開く必要があるらしい。よって、Unityからブラウザを開く+開いたページをSeleniumで操作するという方針に決定。

以下の二つから構成される。

  • 表示するページ(HTML+javascript)
    • GoogleMapsJavaScriptAPIを用いてグーグルストリートビューの表示を行う。
    • 「ボタンを押すと前進する」という機能だけ持つ。
  • Unity側
    • selenium+chromeDriverを用いてページを開く。
    • ジョイコンの傾きを検知し、歩数をカウントする。歩くたびにwebページのボタンを押す。

表示するページ

参考サイト:

GoogleStreetViewが勝手に動く!?GoogleMapAPIを使った自動再生プログラムを作ってみる。|株式会社アーティス
https://www.asobou.co.jp/blog/web/streetview

まずAPIキーを取得します。このページ等を参考にしてGoogleMapsJavaScriptAPIの有効化とキーの取得を行ってください。googleのAPIには無料枠があり、月額200ドル分まで(およそ14000回ほど)無料で使えるようです。

表示するページは以下のように作成します。これはほとんど参考サイトで書かれたコードのままですが、ボタンを追加してそれを押すたびに(5回に1回)移動する、というように書き換えました。また、この記事を参考に多少座標がずれても修正できるようにしています。

<html>
<head>
    <title>StreetView</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <input type="button" onclick="proceed()" id="count" value="0歩">
    <style>
        html,
        body {
            height: 100%;
            margin: 0;
            padding: 0;
        }

        #street-view {
            height: 100%;
        }
    </style>
</head>
<body>

<div id="street-view"></div>


<script>
    const LIMIT = 300; // 移動回数の上限値
    const START_LAT_LNG = {lat: 任意の緯度, lng: 任意の経度}; // 開始地点の緯度、経度
    //const START_LAT_LNG = {lat: 35.1010033, lng: 139.0779688}; // 例
    const START_HEADING = 180; // 開始時の方角
    let panorama;
    isProceed = false;
    let count = 0;
    let m = 0;
    let n = 5; // 何歩ごとに1進むか。要検討。少ない方が嬉しいが無料枠に注意
    let buttonText = document.getElementById('count');
    function initMap() {
        let Links, count = 0, timer_id;
         panorama = new google.maps.StreetViewPanorama(
            document.getElementById('street-view'), {
                position: START_LAT_LNG,
                pov: {
                    heading: START_HEADING,
                    pitch: 0
                },
                zoom: 1
            }); // 
        // 座標の微妙なズレに補正を加える
        svs = new google.maps.StreetViewService();
        svs.getPanoramaByLocation(START_LAT_LNG, 50, function(result, status) {
            if (status == google.maps.StreetViewStatus.OK){
                    panorama.setPosition(result.location.latLng);
            }
        });
        Links = panorama.getLinks();



    }

    function proceed()
    {
        m++;
        if(m >= n)
        {
            m = 0;
            if (panorama.getStatus() == "OK") {
                Links = panorama.getLinks();
                if (count > LIMIT) {
                    alert('stop');
                    return false;
                }
                let target = 0;
                if (Links.length >= 4) {
                    target = Math.floor(Math.random() * Links.length);
                } else {
                    let val = 360;
                    let currentPov = panorama.getPov();
                    Links.forEach(function (element, index) {
                        let ans = Math.abs(currentPov.heading - element.heading);
                        if (val > ans) {
                            val = ans;
                            target = index;
                        }
                    });
                }
                panorama.setPov({
                    heading: Links[target].heading,
                    pitch: 0
                });
                panorama.setPano(Links[target]['pano']);

            };            
        }

        count++;
        buttonText.value = String(count) + "";
    }
</script>

<script src="https://maps.googleapis.com/maps/api/js?key=取得したキー&callback=initMap"
        async defer></script>
</body>
</html>

Unity側の設定

ジョイコン検知のための設定

以下のサイトを参考にしました。

【Unity】Nintendo Switch の Joy-Con を使用する方法

【Unity】Nintendo Switch の Joy-Con のジャイロ・加速度・傾きの値を取得したり、振動させたりすることができる「JoyconLib」紹介

上の内容に従いProject Settingsから入力の変更、二つのライブラリの導入を行いました。

selenium+chromedriverの導入

参考サイト: UnityのC#からSelenium経由でChromeをゴニョゴニョする

上の内容に従い、SeleniumとChromeDriverの導入を行いました。ChromeDriverはバージョンに注意してください。

コード

Start()時にChromeDriverを用いてブラウザを起動。歩行を感知するたびにボタンを押す。

歩数の取得をどうやるかは一考の余地がありますが、とりあえず以下のように設定しました。

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;


public class JoyConWalker : MonoBehaviour
{
    private ChromeDriver _driver;
    private List<Joycon>    m_joycons;
    private Joycon          m_joyconL;
    private Joycon          m_joyconR;
    private Joycon.Button?  m_pressedButtonL;
    private Joycon.Button?  m_pressedButtonR;
    int count = 0;
    float LlastUpper = 0;
    float LlastLower = 0;
    float Llast = 0;
    bool LisUp = true;
    float theta = 0.08f; //閾値。設定は微妙なので要調整。
    void Start()
    {
        var path = Application.streamingAssetsPath;
        _driver = new ChromeDriver(path);

        _driver.Navigate().GoToUrl(Application.dataPath + "/StreetView.html");

        m_joycons = JoyconManager.Instance.j;
        if ( m_joycons == null || m_joycons.Count <= 0 ) return;
        m_joyconL = m_joycons.Find( c =>  c.isLeft );
        m_joyconR = m_joycons.Find( c => !c.isLeft );
    }

    void Update()
    {
        var orientationL = m_joyconL.GetVector();
        var current = orientationL.z;
        if(LisUp)
        {
            if(current >= Llast)
            {
                LlastUpper = current;
            }
            else
            {
                if(LlastUpper - LlastLower >= theta)
                {
                    Proceed();
                }
                LisUp = false;
                LlastLower = current;
            }
        }
        else
        {
            if(current <= Llast)
            {
                LlastLower = current;
            }
            else
            {
                if(LlastUpper - LlastLower >= theta)
                {
                    Proceed();
                }
                LisUp = true;
                LlastUpper = current;
            }            
        }
        Llast = current;


    }

    void Proceed()
    {
        count++;
        Debug.Log(count);
        _driver.FindElement(By.Id("count")).Click();

    }

    void OnDestroy()
    {
        _driver.Dispose();
    }
}

Unityで組み立てる

シーンに空のゲームオブジェクトを配置して↑のcsファイルと、JoyConManager.csをつける。htmlファイルをAssets下に配置。

これで完成。起動し、ページが開いたら左のジョイコンをレッグバンドに入れて足踏みをします。

座標は自分の好きな観光地である熱海に設定。歩数表示が左上に出ています。ステッパーで足踏みしてる間、延々と熱海をさまようことができます。

rapture_20201125221939.jpg
Asset下の構成はgitにアップロードしておきました。

https://github.com/NNNiNiNNN/JoyCon_StreetView

課題

初期座標の決定が難しいこと

現在は完全に固定した座標からスタートして、(アルゴリズム的に)完全に固定されたルートを通るようになっています。座標の決定を完全にランダムにしてから最寄りのデータを持つ座標に補正する、というやり方も試してみましたが、この手段で得られるパノラマ座標は「ストリートビューとして移動できるパノラマ座標」に限らないため、移動先を失うパターンが多くありました。apiも他にどのようなものが使えるのか把握していないため、自分がすぐ直すのは難しそうです。理想を言えば初期地点と目的地点を手動で決定して自動でルート案内するのがいいと思います。実装も多分可能ですが、今回はなしで。

歩行の検知

単純にジョイコンの傾きの差を取ればいいだけ、だとは思うのですがいまいち最適化できていません。そもそもxyzのどれを取ればいいのかもはっきり決め切れていません。現在は動きの閾値を小さめに設定することでだいたいの動きを無理やり取る形にしています。

ジョイコンの接続エラー?

一度接続したジョイコンは、その接続が途切れた後はPCを再起動しないと再接続できなくなることがあります。理由も再現性もよくわかっていません。

あとがき

課題は多くありますが、よく見るフィットネスバイク+ストリートビュー....の亜種を自力で作れたことが嬉しいです。ジョイコンとかgoogleのAPIとか、見渡せば色々便利なものがあるんですねえ。苦戦した部分もありましたが、最終的には簡単な内容で仕上がったのもよかったです。

作ったもので運動するかはわかりません。

参考

GoogleStreetViewが勝手に動く!?GoogleMapAPIを使った自動再生プログラムを作ってみる。|株式会社アーティス

Googleマップストリートビューで目的地に最寄りのストリート位置(座標情報)を取得する方法

【Unity】Nintendo Switch の Joy-Con を使用する方法

【Unity】Nintendo Switch の Joy-Con のジャイロ・加速度・傾きの値を取得したり、振動させたりすることができる「JoyconLib」紹介

UnityのC#からSelenium経由でChromeをゴニョゴニョする

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

UTC ベースで時間データを扱う: JavaScript 版 (IoTプログラミング)

「UTC ベースで時間データを扱う(IoTプログラミング)」
https://qiita.com/nanbuwks/items/e3714f6b0b6b0b8585f4

では C で UTC を扱う記事を書きました。

今回は JavaScript でUTCを扱う方法です。

UNIX 時間 → Date オブジェクト

UNIX時間 1606323269 ( UTC 2020年11月25日 16:54:29 ) を worktime に格納しているとします。

これを JavaScript の Date スクリプトにして、更に文字列形式に変換し出力するには以下のようにします。

      worktime=1606323269;
      datetime = new Date(worktime*1000);
      stringline=(datetime.toLocaleString());
      document.write(stringline);

Dataオブジェクト内部には1970年1月1日からの時間が UTC ベースで格納されています。
UNIX 時間も1970年1月1日からの UTC ベースのカウントですが、UNIX 時間は秒単位なのに対し Date オブジェクトはミリ秒単位なので 1000 倍します。

これの出力は

2020/11/26 1:54:29

となります。日本の Timezone になって出てきましたね。

UTCとする場合には

      stringline=(datetime.toUTCString());

とし、出力は

Wed, 25 Nov 2020 16:54:29 GMT

となります。

しかしこの形式は日本でよく使われる YYYY/MM/DD hh: mm:ss 形式ではないですし、数値で月を表示して IoT で処理しやすくしたい場合もあります。

UTC でなおかつ日本形式にしたい場合は先の toLocaleString を使い、

      stringline=(datetime.toLocaleString('ja-JP', { timeZone: 'UTC' }));

とすると

2020/11/25 16:54:29

が取得できます。

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

TypeScript/JavaScriptの諸々の演算子達(!, ?, &, =)の役割をまとめてみた(初心者向け)

React(TypeScript)、「!」とか「?」とか「&」とか記号多すぎて訳わからんのだが!!
というところからこの記事を書くに至りました。

記号ってただでさえググりづらいし、TS/JS関連だとOptional Chainingとか、Nullish Coalescingとか名前もいちいち覚えづらい…
ので自分用のメモとしても残しておきたいな、と。

記事の内容としては、下記の通り演算子周りが主となります。
• 短絡評価 (||演算子と&&演算子)
• &&演算子
• !(非nullアサーション演算子)
• ?. (Optional Chaining)
• == と=== (等価演算子)
• 三項演算子

初心者ゆえにざっくりとした理解に重きを置いてますので悪しからず。。

短絡評価 (ショートサーキット評価) ー ||演算子と&&演算子 ー

AND演算子(&&)、OR演算子(||)自体はどの言語でもあるかと思いますが、
Reactだとこの短絡評価をよく見かけます。(で、これが正直読みづらい…)
ナウい書き方らしいので頑張りましょう。

短絡評価(ショートサーキット評価)とは
左辺を評価した時点で結果が確定した時は、右辺が評価されないという特性のこと。
以下で||演算子と、 &&演算子でそれぞれ見てみる。

「||演算子 (OR)」 の時

論理演算子||の左側がtrueであれば左側の値、そうでなけれな右側を返す。
(反対に、左側がfalseなら右側を返す)

簡潔に書くと
左辺がtrue/truthyなら左辺を返す
左辺がfalse/falsyなら右辺を返す
(左辺がfalseだったら、右辺も見にいくみたいな感覚でいます。)

ややこしいのが、ショートサーキット評価では、bool値以外の値(普通の文字列とか数値とか)である場合は、暗黙に型変換して評価している、ということ。
(値がtruthy/falsyか、という評価観点)

例えば…数値だと0がfalsy、文字列だと””空文字がfalsyとして評価されます。
つまり、 falsy として定義された値 (つまり、false, 0, -0, 0n, “”, null, undefined, NaN)はfalse判定、それ以外は全てture判定。

実際の処理を見るとなんとなく分かってくる…かもしれません。

10 || false;       // 10
10 || true;        // 10
10 || 20;          // 10
false || 20;       // 20
0 || "zero";       // "zero" 「0」はfalsyなので、"zero"を返す

"Cat" || "Dog";    // "Cat" 
"Cat" || false;    // "Cat"
false || "Cat";    // "Cat" 
"" || "Cat";       // "Cat" 「""」はfalsyなので、"Cat"を返す

なるほど、だんだん分かってきた気がする。

これを活用すると、条件分岐のif/elseを省略して書くことができて、実務だと下記のような使われ方をしてます。
(後ほど書く??演算子も同様)

// 変数 user の値がtruthyならname, そうでなければ文字列'名無し'が変数userにセットされる

// 条件分岐省略前
let user;

if (user) {
   user = name;
} else{
   user = '名無し';
}

// 条件分岐省略後
let user = user || '名無し';   //userが存在すればuserを、なければ'名無し'を代入

・参考
Truthy - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
JavaScript 論理演算子OR「||」の特殊な用法 | プログラミングアカデミー ゼロプラスワン

「&&演算子 (AND)」 の時 

A AND Bという論理式があったとして、
Aがfalseの時点で式全体の結果はfalseで確定するので、Bがどうであるかについてはチェックしない
というのがショートサーキット評価の考え方です。

簡潔に書くと、
左辺がtrue/truthyなら右辺を返す
左辺がfalse/falsyなら左辺を返す
(左辺がtrueだったら、右辺に行ける、みたいな感覚ですね。)

true  && true;     // true 
true  && false;    // false
false && true;     // false
"Cat" && "Dog";    // "Dog" "Cat"はtrutyだから左辺を返す
false && "Cat";    // false
"Cat" && false;    // false
"" && "Cat";       // ""    「""(空文字)」はfalsyだから左辺を返す
0 && 10;           // 0     「0」はfalsyだから左辺を返す

Reactでは結構下記のような感じで使われてます。

list && list.forEach(item) => {処理}

listがfalsyだったらReactは描画せず、trutyだったら処理を行って描画する、といった感じでしょうか。
可読性の観点で下記記事では見事にクソコードと罵られていますが、、笑

JavaScript 論理演算子AND「&&」の特殊な用法(Reactでよく見る記法) | プログラミングアカデミー ゼロプラスワン

||演算子と&&演算子まとめ

とりあえず、
 ||演算子は左辺がfalseだったら右辺を見に行く(返す)!
 &&演算子は左辺がtrueだったら右辺を見に行く(返す)!
というのが重要になりそうです。

・参考
JavaScriptの「&&」「||」について盛大に勘違いをしていた件 - Qiita

「??」演算子 ー Null合体演算子 / Nullish Coalescing ー

左辺が 「nullishな値の時だけ」、右辺が評価される 。
(上記のOR演算子|| と似てるけどちょっと違う。)

違いを明確に…
•  ??演算子:左辺がnullish(=nullまたはundefined)のときだけ、右辺が評価
•  || 演算子:左辺がfalsy(false, 0, -0, 0n, “”, null, undefined, NaN)のときに、右辺が評価

console.log(0 || 1); // 1
console.log(0 ?? 1); // 0

console.log('' || '空文字'); // 空文字
console.log('' ?? '空文字'); // (何も表示されない)

要は、??演算子の方がより厳密に undefined や null を判定してくれるものと覚えておけば良さそう。

「?.」でアクセス ー Optional Chaining ー

obj?.foo

Rubyのぼっち演算子的な役割。(やったぜ)
プロパティのアクセス先が「nullishな値(null または undefined)」の場合、
通常の.でアクセスすると落ちるが、?.だとundefinedが返るので回避できる。

?.でプロパティアクセスしていって、上記の??演算子と組み合わせると、nullishだったら、〜〜(処理)みたいに書けて便利らしい。

・参考
そろそろJavaScriptに採用されそうなOptional Chainingを今さら徹底解説 - Qiita

プロパティ/変数アクセスの「!」 ー 非nullアサーション演算子/ Non-null assertion operator ー

item.color!.toUpperCase()

ここでのcolor!みたいなやつです。

!をつけることで、対象プロパティ/変数がnon-nullであるということをコンパイラに明示するもの。
…要は、『こいつは絶対に null も undefined も入らないよ!』とコンパイラを強引に黙らせ、コンパイルエラーを回避します。
最初これをぼっち演算子(&.)と似たものと思ったのですが、これはあくまでコンパイラを黙らせるだけで、実際にNullがきたら落ちるので注意。
(ちなみに『りあクト!』では極力使うな、と書いてありましたが、現PJでは結構見かける。。)

・参考
TypeScriptのコードレビューを依頼された人のための!と?の解説 | Developers.IO
strictNullChecks - TypeScript Deep Dive 日本語版

条件演算子 (三項演算子)

If~else文の省略バージョン。
普通にRubyでも書けるけど、なぜかRailsのPJより多く使ってる印象。

// 変数 = 条件式 ? trueの時の値/処理 : falseの時の値/処理;
let age = 28
let value = (age >= 30) ? "over thirty" : "under thirty"

「==」 と 「===」 ー 等価演算子と厳密等価演算子 ー

「==」 等価演算子

ご存知の通り、「A == B」でAとBが等しいかどうか、という意味ですが、
等価演算子では暗黙な型変換が行われます。

1 == "1"            // true 文字列を数値に変換後、比較
true == 1           // true 真偽値を数値に変換後、比較
"" == 0             // true 文字列を数値に変換後、比較
null == undefined   // true

等価演算子は両辺の型を解釈し、異なる型の場合に同じ型に変換をしてくれる

「===」 厳密等価演算子

そして厳密等価演算子はと言うと、型も含めて厳密に比較します。

1 == "1"            // false 
true == 1           // false 
"" == 0             // false
null == undefined   // false

===のほうが厳密に比較してくれるってことですね。

if(hoge)

記事に載せるかちょっと迷ったものの、これもtruthy/falsy絡みでもあるので。

if(hoge)のとき
→ hogeがfalsyな値(null, undefined, 0, 空文字(''), false)以外の時は、true判定
  要はちゃんと値があればtrue判定

if(!hoge)のとき
→ hogeがfalsyな値(null, undefined, 0, 空文字(''), false)の時はtrue判定

「|」 (パイプライン一本) ー ユニオン型 ー

これはユニオン型(union type)を定義するときに使うもの。
| で並べることで、複合的な型を定義できる。

// idは数値でも文字列でもOK
let id: number | string
// nullも許すstring型
type nullableString = string | null;

TypeScriptのはなし : ユニオン型がけっこうイイね - 檜山正幸のキマイラ飼育記 (はてなBlog)

以上です。
あくまで初心者による初心者向けの内容なので、より深い知識をお求めの際は参考記事等々読んでくださいませ。
そもそも理解間違ってる、という部分あればなんなりとご指摘ください!

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

【初心者向け】TypeScript/JavaScriptで頻出する演算子(!, ?, &, =)の用法をまとめてみた

React(TypeScript)、「!」とか「?」とか「&」とか記号多すぎて訳わからんのだが!!
というところからこの記事を書くに至りました。

記号ってただでさえググりづらいし、TS/JS関連だとOptional Chainingとか、Nullish Coalescingとか名前もいちいち覚えづらい…
ので自分用のメモとしても残しておきたいな、と。

記事の内容としては、下記の通り演算子周りが主となります。
• 短絡評価 (||演算子と&&演算子)
• &&演算子
• !(非nullアサーション演算子)
• ?. (Optional Chaining)
• == と=== (等価演算子)
• 三項演算子

初心者ゆえにざっくりとした理解に重きを置いてますので悪しからず。。

短絡評価 (ショートサーキット評価) ー ||演算子と&&演算子 ー

AND演算子(&&)、OR演算子(||)自体はどの言語でもあるかと思いますが、
Reactだとこの短絡評価をよく見かけます。(で、これが正直読みづらい…)
ナウい書き方らしいので頑張りましょう。

短絡評価(ショートサーキット評価)とは
左辺を評価した時点で結果が確定した時は、右辺が評価されないという特性のこと。
以下で||演算子と、 &&演算子でそれぞれ見てみる。

「||演算子 (OR)」 の時

論理演算子||の左側がtrueであれば左側の値、そうでなけれな右側を返す。
(反対に、左側がfalseなら右側を返す)

簡潔に書くと
左辺がtrue/truthyなら左辺を返す
左辺がfalse/falsyなら右辺を返す
(左辺がfalseだったら、右辺も見にいくみたいな感覚でいます。)

ややこしいのが、ショートサーキット評価では、bool値以外の値(普通の文字列とか数値とか)である場合は、暗黙に型変換して評価している、ということ。
(値がtruthy/falsyか、という評価観点)

例えば…数値だと0がfalsy、文字列だと””空文字がfalsyとして評価されます。
つまり、 falsy として定義された値 (つまり、false, 0, -0, 0n, “”, null, undefined, NaN)はfalse判定、それ以外は全てture判定。

実際の処理を見るとなんとなく分かってくる…かもしれません。

10 || false;       // 10
10 || true;        // 10
10 || 20;          // 10
false || 20;       // 20
0 || "zero";       // "zero" 「0」はfalsyなので、"zero"を返す

"Cat" || "Dog";    // "Cat" 
"Cat" || false;    // "Cat"
false || "Cat";    // "Cat" 
"" || "Cat";       // "Cat" 「""」はfalsyなので、"Cat"を返す

なるほど、だんだん分かってきた気がする。

これを活用すると、条件分岐のif/elseを省略して書くことができて、実務だと下記のような使われ方をしてます。
(後ほど書く??演算子も同様)

// 変数 user の値がtruthyならname, そうでなければ文字列'名無し'が変数userにセットされる

// 条件分岐省略前
let user;

if (user) {
   user = name;
} else{
   user = '名無し';
}

// 条件分岐省略後
let user = user || '名無し';   //userが存在すればuserを、なければ'名無し'を代入

・参考
Truthy - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
JavaScript 論理演算子OR「||」の特殊な用法 | プログラミングアカデミー ゼロプラスワン

「&&演算子 (AND)」 の時 

A AND Bという論理式があったとして、
Aがfalseの時点で式全体の結果はfalseで確定するので、Bがどうであるかについてはチェックしない
というのがショートサーキット評価の考え方です。

簡潔に書くと、
左辺がtrue/truthyなら右辺を返す
左辺がfalse/falsyなら左辺を返す
(左辺がtrueだったら、右辺に行ける、みたいな感覚ですね。)

true  && true;     // true 
true  && false;    // false
false && true;     // false
"Cat" && "Dog";    // "Dog" "Cat"はtrutyだから左辺を返す
false && "Cat";    // false
"Cat" && false;    // false
"" && "Cat";       // ""    「""(空文字)」はfalsyだから左辺を返す
0 && 10;           // 0     「0」はfalsyだから左辺を返す

Reactでは結構下記のような感じで使われてます。

list && list.forEach(item) => {処理}

listがfalsyだったらReactは描画せず、trutyだったら処理を行って描画する、といった感じでしょうか。
可読性の観点で下記記事では見事にクソコードと罵られていますが、、笑

JavaScript 論理演算子AND「&&」の特殊な用法(Reactでよく見る記法) | プログラミングアカデミー ゼロプラスワン

||演算子と&&演算子まとめ

とりあえず、
 ||演算子は左辺がfalseだったら右辺を見に行く(返す)!
 &&演算子は左辺がtrueだったら右辺を見に行く(返す)!
というのが重要になりそうです。

・参考
JavaScriptの「&&」「||」について盛大に勘違いをしていた件 - Qiita

「??」演算子 ー Null合体演算子 / Nullish Coalescing ー

左辺が 「nullishな値の時だけ」、右辺が評価される 。
(上記のOR演算子|| と似てるけどちょっと違う。)

違いを明確に…
•  ??演算子:左辺がnullish(=nullまたはundefined)のときだけ、右辺が評価
•  || 演算子:左辺がfalsy(false, 0, -0, 0n, “”, null, undefined, NaN)のときに、右辺が評価

console.log(0 || 1); // 1
console.log(0 ?? 1); // 0

console.log('' || '空文字'); // 空文字
console.log('' ?? '空文字'); // (何も表示されない)

要は、??演算子の方がより厳密に undefined や null を判定してくれるものと覚えておけば良さそう。

「?.」でアクセス ー Optional Chaining ー

obj?.foo

Rubyのぼっち演算子的な役割。(やったぜ)
プロパティのアクセス先が「nullishな値(null または undefined)」の場合、
通常の.でアクセスすると落ちるが、?.だとundefinedが返るので回避できる。

?.でプロパティアクセスしていって、上記の??演算子と組み合わせると、nullishだったら、〜〜(処理)みたいに書けて便利らしい。

・参考
そろそろJavaScriptに採用されそうなOptional Chainingを今さら徹底解説 - Qiita

プロパティ/変数アクセスの「!」 ー 非nullアサーション演算子/ Non-null assertion operator ー

item.color!.toUpperCase()

ここでのcolor!みたいなやつです。

!をつけることで、対象プロパティ/変数がnon-nullであるということをコンパイラに明示するもの。
…要は、『こいつは絶対に null も undefined も入らないよ!』とコンパイラを強引に黙らせ、コンパイルエラーを回避します。
最初これをぼっち演算子(&.)と似たものと思ったのですが、これはあくまでコンパイラを黙らせるだけで、実際にNullがきたら落ちるので注意。
(ちなみに『りあクト!』では極力使うな、と書いてありましたが、現PJでは結構見かける。。)

・参考
TypeScriptのコードレビューを依頼された人のための!と?の解説 | Developers.IO
strictNullChecks - TypeScript Deep Dive 日本語版

条件演算子 (三項演算子)

If~else文の省略バージョン。
普通にRubyでも書けるけど、なぜかRailsのPJより多く使ってる印象。

// 変数 = 条件式 ? trueの時の値/処理 : falseの時の値/処理;
let age = 28
let value = (age >= 30) ? "over thirty" : "under thirty"

「==」 と 「===」 ー 等価演算子と厳密等価演算子 ー

「==」 等価演算子

ご存知の通り、「A == B」でAとBが等しいかどうか、という意味ですが、
等価演算子では暗黙な型変換が行われます。

1 == "1"            // true 文字列を数値に変換後、比較
true == 1           // true 真偽値を数値に変換後、比較
"" == 0             // true 文字列を数値に変換後、比較
null == undefined   // true

等価演算子は両辺の型を解釈し、異なる型の場合に同じ型に変換をしてくれる

「===」 厳密等価演算子

そして厳密等価演算子はと言うと、型も含めて厳密に比較します。

1 == "1"            // false 
true == 1           // false 
"" == 0             // false
null == undefined   // false

===のほうが厳密に比較してくれるってことですね。

if(hoge)

記事に載せるかちょっと迷ったものの、これもtruthy/falsy絡みでもあるので。

if(hoge)のとき
→ hogeがfalsyな値(null, undefined, 0, 空文字(''), false)以外の時は、true判定
  要はちゃんと値があればtrue判定

if(!hoge)のとき
→ hogeがfalsyな値(null, undefined, 0, 空文字(''), false)の時はtrue判定

「|」 (パイプライン一本) ー ユニオン型 ー

これはユニオン型(union type)を定義するときに使うもの。
| で並べることで、複合的な型を定義できる。

// idは数値でも文字列でもOK
let id: number | string
// nullも許すstring型
type nullableString = string | null;

TypeScriptのはなし : ユニオン型がけっこうイイね - 檜山正幸のキマイラ飼育記 (はてなBlog)

以上です。
あくまで初心者による初心者向けの内容なので、より深い知識をお求めの際は参考記事等々読んでくださいませ。
そもそも理解間違ってる、という部分あればなんなりとご指摘ください!

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

WebSocket メモ

WebSocket とは

  • クライアントとリモートホストの間で双方向通信を可能とするプロトコル。
  • セキュリティモデルはウェブブラウザでよく用いられるオリジンをベースとしたモデル。
  • XMLHttpRequest やロングポーリングに頼らずにサーバとの双方向通信を必要とするブラウザアプリケーションのためのメカニズムを提供する。
  • クライアント・サーバ間の双方向のトラフィックのためにTCPコネクションを一つだけ張る。

プロトコルの概要

プロトコルはハンドシェイクデータ転送の2つの部分からなる。

ハンドシェイク

クライアント -> サーバ

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

サーバ -> クライアント

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

ハンドシェイクが成功すると、クライアントとサーバは1つ以上のフレームからなるメッセージをお互いに送出し合うことになる。

Node.js でシンプルな WebSocket 通信

Node.js では、ws というWebSocketライブラリを用いてシンプルな WebSocket 通信ができる(Socket.IO でも可能)。

サーバ側のコード

const WebSocket = require('ws')

const server = new WebSocket.Server({ port: 3000 })

server.on('connection', ws => {
  ws.send('connected!')

  ws.on('message', message => {
    console.log('received: %s', message)
  })
})

クライアント側のコード

const ws = require('ws')

const client = new ws('ws://localhost:3000')

client.on('open', () => {
  client.send('hello')
})

client.on('message', message => {
  console.log(message)
})

node server.js でWebSocketサーバを立ち上げた後 node client.js を実行すると、サーバ側では

received: hello

と出力され、クライアント側では

connected!

と出力される。

WebSocket でチャットアプリを作る

blessed というクールな CUI を作れるライブラリを使って、超シンプルなチャットアプリ(もどき)を作ってみる。

サーバ側

const WebSocket = require('ws')

const server = new WebSocket.Server({ port: 3000 })

const sockets = new Map()

const broadcast = message => {
  console.log(message)
  server.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message)
    }
  })
}

server.on('connection', socket => {
  socket.on('message', message => {

    if (sockets.has(socket)) {
      const name = sockets.get(socket)
      broadcast(`${name}: ${message}`)
    } else {
      sockets.set(socket, message)
      broadcast(`${message} entered the room.`)
    }
  })

  socket.on('close', () => {
    const name = sockets.get(socket)
    if (name) {
      sockets.delete(socket)
      broadcast(`${name} left the room.`)
    }
  })
})

クライアント側

const ws = require('ws')
const blessed = require('blessed')

const screen = blessed.screen({ smartCSR: true })
screen.title = 'chat'

const chatList = blessed.list({ border: { type: 'line' } })

const textbox = blessed.textbox({
  height: '10%',
  bottom: 0,
  input: true,
  inputOnFocus: true,
  border: {
    type: 'line'
  }
})

const client = new ws('ws://localhost:3000')

client.on('open', async () => {
  textbox.setText('(Please enter your name)')
  textbox.focus()
  screen.render()
})

client.on('message', message => {
  chatList.insertLine(0, message)
  screen.render()
})

textbox.key(['enter'], () => {
  client.send(textbox.getText())
  textbox.clearValue()
  chatList.render()
  textbox.focus()
})


const EXIT_COMMANDS = ['escape', 'C-c']
const exit = () => process.exit(0)
screen.key(EXIT_COMMANDS, exit)
chatList.key(EXIT_COMMANDS, exit)
textbox.key(EXIT_COMMANDS, exit)


screen.append(chatList)
screen.append(textbox)
screen.render()

実際の動作

chat.gif

それっぽくは動く。

所感

  • blessed の API がよくわからない
  • ブラウザを UI としたチャットアプリを作ってみたい
  • 随時更新する

出典

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

JavaScript アウトプット

・変数宣言(var,let,const)をしないとすべてグローバル変数になる(関数で厄介)。

・Functionコンストラクター(new Finction)は、関数内部でもその配下の変数は全てグローバルスコープとみなされるので、利用はおすすめしない。

・与える因数の数が、関数側で要求する数と異なる場合も、チェックしない(エラーにならない)。

・引数は、後ろの引数だけ省略できる。

・高階関数で配列をリスト化したり合計値を算出したりできる。

・スコープチェーンとは、グローバルオブジェクトとCallオブジェクト(ローカル変数)を生成の順に連結したリスト。変数のメカニズムがわかる。

・関すリテラルは、匿名(無名)関数を使用する。

・closure関数が呼び出されると、
 ・匿名関数(関数名を記述せずに引数や代入に使用する関数 
  例:const myfunc = function (name))を表すCallオブジェクト(ローカル変数)
 ・closure関数のCallオブジェクト
 ・グローバルオブジェクト
 というスコープチェーンが形成される。

・プロパティの宣言→コンストラクターで・メソッドの宣言→プロトタイプで

・プロパティの宣言 → コンストラクターで

・メソッドの宣言 → プロトタイプで

・静的メソッド、プロパティ ↔︎ インスタンスメソッド、プロパティ

・グローバル変数はできるだけ減らすこと(多いとややこしくなる)、そのために静的メンバーを使うこと

・プロトタイプチェーン・・・暗黙の参照で継承関係している。終端には必ず、Object.Prototypeがある。インスタンスが生成された時点で固定され、その後の変更にかかわらず保存される。

・querySelector(All)はより細かいものまで特定できるが、低速なので単純なものはgetElemnt~にする。

・タグ内のJSは、イベントハンドラーの呼び出し程度にすること。

・ページの初期化処理は、DOMContentLoadedイベントリスナーで表すのが基本

・getAttribute/setAttributeメソッドは、
 ・HTMLとJavaScriptとで名前の相違を意識する必要がない
 ・(文字列として指定できるので) 取得/設定する属性名を、スクリプトから動的に変更できる

・一般的には、HTML文字列を埋め込むのでなければ、まずはtextContentプロパティを優先して利用することをおすすめ(読み込みの速さなど)

・ユーザーからの入力値など外部からの入力値を、innerHTMLプロパティで出力しないこと(不特定のユーザーのブラウザー上で勝手に実行されてしまう可能性がある)

・引数(ひきすう)とは関数に与える追加情報

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

Javascript(React) 株価取得WebAPI グラフ表示(Function Component)

FireShot Capture 008 - React App - localhost.png

API-GRAPH-REACTという名前で
githubにて公開中です。

検索欄入力の際チィッカーシンボルを入力してください
例:
Amazon => AMZN
Microsofl => MSFT
Apple => APPL
Facebook => FB

#連想配列

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

修正

window.addEventListener('DOMContentLoaded', () => {
    // 「送信」ボタンの要素を取得
    const submit = document.querySelector('#contact-submit');

    // エラーメッセージと赤枠の削除
    function reset(input_infomation, error_message){
        const input_info = document.querySelector(input_infomation);
        const err_message = document.querySelector(error_message);
        err_message.textContent ='';
        input_info.classList.remove('input-invalid');
    };

    // 「お名前」入力欄の空欄チェック関数
    function invalitName(input_target, error_target, error_message){

        const name = document.querySelector(input_target);
        const errMsgName = document.querySelector(error_target);

        if(!name.value){
            errMsgName.classList.add('form-invalid');
            errMsgName.textContent = error_message;
            name.focus();
            name.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「ふりがな」入力欄の空欄チェック関数
    function invalitHurigana(input_target, error_target, error_message){

        const hurigana = document.querySelector(input_target);
        const errMsgHurigana = document.querySelector(error_target);

        if(!hurigana.value){
            errMsgHurigana.classList.add('form-invalid');
            errMsgHurigana.textContent = error_message;
            hurigana.focus();
            hurigana.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;

    };

    // 「郵便番号」入力欄の空欄チェック関数
    function invalitPostal(input_target, error_target, error_message){

        const postal = document.querySelector(input_target);
        const errMsgPostal = document.querySelector(error_target);

        if(!postal.value){
            errMsgPostal.classList.add('form-invalid');
            errMsgPostal.textContent = error_message;
            postal.focus();
            postal.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;

    };

    // 「住所」入力欄の空欄チェック関数
    function invalitAddress(input_target, error_target, error_message){

        const address = document.querySelector(input_target);
        const errMsgAddress = document.querySelector(error_target);

        if(!address.value){
            errMsgAddress.classList.add('form-invalid');
            errMsgAddress.textContent = error_message;
            address.focus();
            address.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「電話番号」入力欄の空欄チェック関数
    function invalitTel(input_target, error_target, error_message){

        const tel = document.querySelector(input_target);
        const errMsgTel = document.querySelector(error_target);

        if(!tel.value){
            errMsgTel.classList.add('form-invalid');
            errMsgTel.textContent = error_message;
            tel.focus();
            tel.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「メールアドレス」入力欄の空欄チェック関数    
    function invalitEmail(input_target, error_target, error_message){

        const email = document.querySelector(input_target);
        const errMsgEmail = document.querySelector(error_target);

        if(!email.value){
            errMsgEmail.classList.add('form-invalid');
            errMsgEmail.textContent = error_message;
            email.focus();
            email.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「会社名」入力欄の空欄チェック関数
    function invalitCompany(input_target, error_target, error_message){

        const company = document.querySelector(input_target);
        const errMsgCompany = document.querySelector(error_target);

        if(!company.value){
            errMsgCompany.classList.add('form-invalid');
            errMsgCompany.textContent = error_message;
            company.focus();
            company.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「お問い合わせ内容」入力欄の空欄チェック関数
    function invalitContent(input_target, error_target, error_message){

        const content = document.querySelector(input_target);
        const errMsgContent = document.querySelector(error_target);

        if(!content.value){
            errMsgContent.classList.add('form-invalid');
            errMsgContent.textContent = error_message;
            content.focus();
            content.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };


    // 「送信」ボタンの要素にクリックイベントを設定する
    submit.addEventListener('click', (e) => {
        // デフォルトアクションをキャンセル
        e.preventDefault();

        reset('#name-js', '#err-msg-name');
        reset('#hurigana-js', '#err-msg-hurigana');
        reset('#postal-js', '#err-msg-postal');
        reset('#address-js', '#err-msg-address');
        reset('#tel-js', '#err-msg-tel');
        reset('#email-js', '#err-msg-email');
        reset('#company-js', '#err-msg-company');
        reset('#content-js', '#err-msg-content');

        const focus = () => document.querySelector('#name-js').focus();

        // 「お名前」入力欄の空欄チェック
        if(invalitName('#name-js', '#err-msg-name', 'お名前が入力されていません')===false){
            return;
        };
        // 「ふりがな」入力欄の空欄チェック
        if(invalitHurigana('#hurigana-js', '#err-msg-hurigana', '入力必須です')===false){
            return;
        };

        // ひらがなチェック
        const hurigana = document.querySelector("#hurigana-js");
        const errMsgHurigana = document.querySelector("#err-msg-hurigana");
        const huriganaCheck = /[^ぁ-んー  ]/u; 
        if(hurigana.value.match(huriganaCheck)){
            errMsgHurigana.classList.add('form-invalid');
            errMsgHurigana.textContent = 'ひらがなで入力してください';
            hurigana.focus();
            hurigana.classList.add('input-invalid');
            return;
        }else{
            errMsgHurigana.textContent ='';
            hurigana.classList.remove('input-invalid');
            hurigana.blur();
        };

        // 「郵便番号」入力欄の空欄チェック
        if(invalitPostal('#postal-js', '#err-msg-postal', '入力必須です')===false){
            return;
        };

        // 郵便番号形式チェック
        const postal = document.querySelector("#postal-js");
        const errMsgPostal = document.querySelector("#err-msg-postal");
        const postalCheck = /([0-9]{7})$/; 
        // const postalCheck = /^\d{7}$/; 
        if(postal.value.match(postalCheck)){
            errMsgPostal.textContent ='';
            postal.classList.remove('input-invalid');
            postal.blur();
        }else{
            errMsgPostal.classList.add('form-invalid');
            errMsgPostal.textContent = '郵便番号は数字7桁で入力してください';
            postal.focus();
            postal.classList.add('input-invalid');
            return;
        };

        // 「住所」入力欄の空欄チェック
        if(invalitAddress('#address-js', '#err-msg-address', '入力必須です')===false){
            return;
        };
        // 「電話番号」入力欄の空欄チェック
        if(invalitTel('#tel-js', '#err-msg-tel', '入力必須です')===false){
            return;
        };

        //電話番号形式チェック
        const tel = document.querySelector("#tel-js");
        const errMsgTel = document.querySelector("#err-msg-tel");
        const telCheck = /0\d{1,4}\d{1,4}\d{4}/; 
        const telCheckNotNumber = /\D/; 
        if(tel.value.match(telCheckNotNumber)){
            errMsgTel.classList.add('form-invalid');
            errMsgTel.textContent = '電話番号は数字で入力してください';
            tel.focus();
            tel.classList.add('input-invalid');
            return;
        }else if(!tel.value.match(telCheck)){
            errMsgTel.classList.add('form-invalid');
            errMsgTel.textContent = '電話番号の形式が違います';
            tel.focus();
            tel.classList.add('input-invalid');
            return;
        }else{
            errMsgTel.textContent ='';
            tel.classList.remove('input-invalid');
            tel.blur();
        };

        // 「メールアドレス」入力欄の空欄チェック
        if(invalitEmail('#email-js', '#err-msg-email', '入力必須です')===false){
            return;
        };

        // Email形式チェック
        const email = document.querySelector("#email-js");
        const errMsgEmail = document.querySelector("#err-msg-email");
        const emailCheck = /^[-a-z0-9~#&'*/?`\|!$%^&*_=+}{\'?]+(\.[-a-z0-9~#&'*/?`\|!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)|(docomo\ezweb\softbank)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i; 
        if(email.value.match(emailCheck)){
            errMsgEmail.textContent ='';
            email.classList.remove('input-invalid');
            email.blur();
        }else{
            errMsgEmail.classList.add('form-invalid');
            errMsgEmail.textContent = 'Emailの形式で入力してください';
            email.focus();
            email.classList.add('input-invalid');
            return;
        };

        // 「会社名」入力欄の空欄チェック
        if(invalitCompany('#company-js', '#err-msg-company', '入力必須です')===false){
            return;
        };
        // 「お問い合わせ内容」入力欄の空欄チェック
        if(invalitContent('#content-js', '#err-msg-content', '入力必須です')===false){
            return;
        };

        document.customerinfo.submit();

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

[react] 多次元配列をテーブルに挿入するfunction component

オライリージャパンreatビギナーズガイドの練習課題『高機能excel作成を作ろう』にて
多次元配列の処理をclass componentで書かれていた。
function componentの理解のため、function componentに書き換えていく。

参考書籍:reactビギナーズガイド
サンプルコード:https://github.com/stoyan/reactbook
該当フォルダ・ファイル:chapters/03.04.table-sortby.html

FireShot Capture 003 - React App - localhost.png
配列の要素は適当に違う値を使用します。

Table.jsx
function Table() {

  const heads = ["", "", "", "", ""]
  const heads1 = heads.map((head) =>
    <th>{head}</th>
  )

  const data = [
           [1, 2, 3, 4, 5], 
           [6, 7, 8, 9, 10,], 
           [11, 12, 13, 14, 15], 
           [16, 17, 18, 19, 20,], 
           [21, 22, 23, 24, 25], 
           [26, 27, 28, 29, 30,]
               ]



  return (
    <div>
      <table border='1' cellSpacing='0'>
        <tr>{heads1}</tr>
        {data.map(row1 =>
          <tr>
            {row1.map(row2 =>
              <td>{row2}</td>
            )}
          </tr>
        )}
      </table>
    </div>
  );
}

export default Table;

(随時更新予定)

#react
#ファンクションコンポーネント
#Table
#多次元配列
#二次元配列
#map関数

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

Javascript(react) 多次元配列をテーブルに挿入するfunction component

オライリージャパンreatビギナーズガイドの練習課題『高機能excel作成を作ろう』にて
多次元配列の処理をclass componentで書かれていた。
function componentの理解のため、function componentに書き換えていく。

参考書籍:reactビギナーズガイド
サンプルコード:https://github.com/stoyan/reactbook
該当フォルダ・ファイル:chapters/03.04.table-sortby.html

FireShot Capture 003 - React App - localhost.png
配列の要素は適当に違う値を使用します。

土日がない?休みなんか必要ないですよね?笑

Table.jsx
function Table() {

  const heads = ["", "", "", "", ""]
  const heads1 = heads.map((head) =>
    <th>{head}</th>
  )

  const data = [
           [1, 2, 3, 4, 5], 
           [6, 7, 8, 9, 10,], 
           [11, 12, 13, 14, 15], 
           [16, 17, 18, 19, 20,], 
           [21, 22, 23, 24, 25], 
           [26, 27, 28, 29, 30,]
               ]



  return (
    <div>
      <table border='1' cellSpacing='0'>
        <tr>{heads1}</tr>
        {data.map(row1 =>
          <tr>
            {row1.map(row2 =>
              <td>{row2}</td>
            )}
          </tr>
        )}
      </table>
    </div>
  );
}

export default Table;

(随時更新予定)

#react
#ファンクションコンポーネント
#Table
#多次元配列
#二次元配列
#map関数

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

【JavaScript】コールバック関数メモ

配列arryに数字を格納し、それを1つずつコンソールに出力させる処理

app.js
const arry = [1, 2, 3, 4]

arry.forEach(function() {
  console.log(val)
})
出力結果
1
2
3
4
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】「dotenv」を使った環境変数の設定方法

「dotenv」をインストール

プロジェクト直下でnpm install --save @nuxtjs/dotenvを実行

ターミナル
$ npm install --save @nuxtjs/dotenv

package.jsonで確認

package.json
  "dependencies": {
    "@nuxtjs/dotenv": "^1.4.1",    <-
    "core-js": "^3.6.5",
    "nuxt": "^2.14.6"
  }

プロジェクト直下に.envファイルを作成

ターミナル
$ touch .env

.envファイルに適当な値を定義します。

.env
TEST = 'テスト'

console.logで確認

きちんと設定されているか確認します。
pages/index.vueconsole.logを設置。

pages/index.vue
<script>
export default {
  created() {
    console.log(process.env.TEST)
  }
}
</script>

スクリーンショット 2020-11-25 21.00.38.png

OKです!

.gitnoreファイルの確認

最後に.gitnoreファイル内に.envの記述があるか確認します。
nuxt-create-appを使用すればデフォルトで記述されますが、
無い場合は記述しておきます。

スクリーンショット 2020-11-25 21.04.17.png

これでgitの管理対象外となります。

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

【Ajax】jQueryを使って一覧ページでいいね機能

はじめに

一覧ページでのいいね機能を実装しようとしましたが、一番上の星をいいねしたら全てのチームにいいねがついてしまうという問題が発生しました。この問題が起きたことで大変学習になったので、思考の整理とアウトプットとして記事にまとめさせていただきました。また、いいね機能は多くのアプリで実装することが多いと思います。同じような問題が起きている方や別の実装方法など、ご意見いただけたら幸いです!

概要

まず大前提にテーブルは、users, teams, likesの3つでlikes
usersteamsの中間テーブルになっています。
AjaxでHTTPメソッドのGET, POST, DELETEができるように実装していきます。

起きている問題・解決したいこと(仮説)

起きている問題と解決したいことを簡単に書いていきたいと思います。

起きている問題

  • 全ての記事にいいねがついてしまう。
  • 全てのチームのidを取得していたつもりが一番上のチームのidしか取得できていない。(デベロッパーツールで確認)

解決したいことと仮説

  • 配列にして取得できていると思ったができていないので配列から一つずつチームを取得できるようにする。
  • いいね対象を判断できるようにクラスをつけないといけない。
  • クリックした対象に動的idが付与されていれば実装できるのではないか。

Ajaxで実装すること

  • いいねの表示
  • いいねされたら黄色い星を表示
  • いいねされているものをクリックしたら白い星を表示

LikesController

show, create, destroyアクションを使用しています。
Railsでの定義と違うところはリクエストに対して返すのがjson形式のデータということです。

LikesController
class LikesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_like

  def show
    like_status = current_user.has_liked?(@team)
    render json: { hasLiked: like_status }
  end


  def create
    @team.likes.create!(user_id: current_user.id)

    render json: { status: 'ok' }
  end

  def destroy
    like = @team.likes.find_by!(user_id: current_user.id)
    like.destroy!

    render json: { status: 'ok' }
  end

  private

  def set_like
    @team = Team.find(params[:team_id])
  end

end

user.rb

has_liked?user.rbで定義しています。showアクション内の処理のようにcurrent_userがteamをlikeしてる?と直感的にわかりやすいと思い、定義しております。

user.rb
def has_liked?(team)
  likes.exists?(team_id: team.id)
end

index.html.haml

HTTPリクエストを送るにはJavaScriptでidを取得できるようにしなくてはいけないので、テンプレートにカスタムデータを記述しています。data-<情報>とすることで任意の属性を付与することができます。今回はチームに対していいねをしたいのでteamidが必要です。
routesを確認するとどこにリクエストを送れば良いかよくわかります。

たとえばですが、以下index.html.hamlのように記述してデベロッパーツールで確認すると

  • data-team-id="1"
  • id="active-star1"

このようにdata属性とチームのidが入ったidを取得することができます。
チームのidが入ったidは一覧ページでのいいね機能なので、クリックしたときに特定のチームをいいねするという状況になります。それを判定するために記述しております。実際にjQueryのコードを見ていただいたほうが早いと思いますが、簡単に説明させていただきますと、active-starというidだけだと、active-starというidは複数あるので、全てのチームにいいねをつける処理が実行されてしまいます。そのため、動的なid(今回はチームのid)を付与しております。数字の1の部分がチームのidです。

ここで重要なのは、

  • カスタムデータ
  • hiddenでいいねの表示を隠していること

hiddenを使っている理由はいいねの状態を確認し表示するということをAjaxで表示するためです。

index.html.haml
.hidden.active-star{id: "active-star#{teams.id}", data: {team_id: tams.id}}
          = image_tag 'star-yellow.png'
.hidden.in-active-star{id: "in-active-star#{teams.id}", data: {team_id: teams.id}}
          = image_tag 'star-white.png'
css
.hidden {
  display: none;
}

jQuery

本題のいいね機能の実装部分です。
記述が冗長になってしまったので、コメントを入れております。重要なのはcsrfTokenをリクエスト時に持たせることです。これをしないと422 (Unprocessable Entity)というエラーが起きます。なぜかというとGETと違ってPOSTなどの処理はデータベースの変更をするリクエストなので、簡単に操作されては困るため制約がついています。そのため、rails-ujsを使ってaxiosでリクエスト時にcsrfTokenというのを持たせるようにしております。

jQuery
import $ from 'jquery'
import axios from 'axios'

import { csrfToken } from 'rails-ujs'
// リクエスト時にCSRFトークンを持たせる
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()

document.addEventListener('DOMContentLoaded', () => {

  // ロード時にいいねされていない星を配列で取得
  $('.in-active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    let likeData = $(element).data()
    // カスタムデータからチームIDを取得
    let getId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${getId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const inActiveStatus = response.data.hasLiked
        // falseであればいいねされていない => 白い星を表示するために、'hidden'を取り外す
        if ( inActiveStatus === false ) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // ロード時にいいねされている星を配列で取得
  $('.active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    let likeData = $(element).data()
    // カスタムデータからチームIDを取得
    let getId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${getId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const activeStatus = response.data.hasLiked
        // trueであればいいねされている => 黄色い星を表示するために、'hidden'を取り外す
        if ( activeStatus === true) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // #create いいねをつけたいときの処理
  $('.in-active-star').on('click', (e) => {
    e.preventDefault();
    let dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    let teamId = dataset.teamId
    // teamIdを使いPOSTリクエストを送る
    axios.post(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#in-active-star${teamId}`).addClass('hidden');
        $(`#active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })

  })

  // #destroy いいねを外したいときの処理
  $('.active-star').on('click', (e) => {
    e.preventDefault();
    let dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    let teamId = dataset.teamId
    // teamIdを使いdeleteメソッドを使う
    axios.delete(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#active-star${teamId}`).addClass('hidden');
        $(`#in-active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })
  })

})

まとめ

  • Ajax処理はデベロッパーツールを使いdebuggerconsole.log()を使い値が取れているか確認をしっかりすると開発が捗る。
  • 詳細ページのように一つしかいいねがないときは意識していなかったが、一覧ページなど複数ある中の一つというように特定したいときは個別のidを付与することで実装できる。
  • POSTやDELETE時にはデータベースの操作をすることからCSRFトークンというものが必要になる。

最後に

今回はjQueryを使って実装しました。Vue.jsの学習も始めたので、生JSやjQueryをもっと理解したら開発に使っていきたいなと思っています。このように実装するのもひとつの方法だと思いますが、他にもたくさんの実装方法があると思います。他のライブラリやフレームワークではどのように実装するのか、また一度書いたコードもリファクタリングを積極的にやっていけたらと思います!

参考文献

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

【Rails】いいね機能をjQueryで実装

はじめに

一覧ページでのいいね機能を実装しようとしましたが、一番上の星をいいねしたら全てのチームにいいねがついてしまうという問題が発生しました。この問題が起きたことで大変学習になったので、思考の整理とアウトプットとして記事にまとめさせていただきました。また、いいね機能は多くのアプリで実装することが多いと思います。同じような問題が起きている方や別の実装方法など、ご意見いただけたら幸いです!

概要

まず大前提にテーブルは、users, teams, likesの3つでlikes
usersteamsの中間テーブルになっています。
AjaxでHTTPメソッドのGET, POST, DELETEができるように実装していきます。

起きている問題・解決したいこと(仮説)

起きている問題と解決したいことを簡単に書いていきたいと思います。

起きている問題

  • 全ての記事にいいねがついてしまう。
  • 全てのチームのidを取得していたつもりが一番上のチームのidしか取得できていない。(デベロッパーツールで確認)

解決したいことと仮説

  • 配列にして取得できていると思ったができていないので配列から一つずつチームを取得できるようにする。
  • いいね対象を判断できるようにクラスをつけないといけない。
  • クリックした対象に動的idが付与されていれば実装できるのではないか。

Ajaxで実装すること

  • いいねの表示
  • いいねされたら黄色い星を表示
  • いいねされているものをクリックしたら白い星を表示

LikesController

show, create, destroyアクションを使用しています。
Railsでの定義と違うところはリクエストに対して返すのがjson形式のデータということです。

LikesController
class LikesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_like

  def show
    like_status = current_user.has_liked?(@team)
    render json: { hasLiked: like_status }
  end


  def create
    @team.likes.create!(user_id: current_user.id)

    render json: { status: 'ok' }
  end

  def destroy
    like = @team.likes.find_by!(user_id: current_user.id)
    like.destroy!

    render json: { status: 'ok' }
  end

  private

  def set_like
    @team = Team.find(params[:team_id])
  end

end

user.rb

has_liked?user.rbで定義しています。showアクション内の処理のようにcurrent_userがteamをlikeしてる?と直感的にわかりやすいと思い、定義しております。

user.rb
def has_liked?(team)
  likes.exists?(team_id: team.id)
end

index.html.haml

HTTPリクエストを送るにはJavaScriptでidを取得できるようにしなくてはいけないので、テンプレートにカスタムデータを記述しています。data-<情報>とすることで任意の属性を付与することができます。今回はチームに対していいねをしたいのでteamidが必要です。
routesを確認するとどこにリクエストを送れば良いかよくわかります。

たとえばですが、以下index.html.hamlのように記述してデベロッパーツールで確認すると

  • data-team-id="1"
  • id="active-star1"

このようにdata属性とチームのidが入ったidを取得することができます。
チームのidが入ったidは一覧ページでのいいね機能なので、クリックしたときに特定のチームをいいねするという状況になります。それを判定するために記述しております。実際にjQueryのコードを見ていただいたほうが早いと思いますが、簡単に説明させていただきますと、active-starというidだけだと、active-starというidは複数あるので、全てのチームにいいねをつける処理が実行されてしまいます。そのため、動的なid(今回はチームのid)を付与しております。数字の1の部分がチームのidです。

ここで重要なのは、

  • カスタムデータ
  • hiddenでいいねの表示を隠していること

hiddenを使っている理由はいいねの状態を確認し表示するということをAjaxで表示するためです。

index.html.haml
.hidden.active-star{id: "active-star#{teams.id}", data: {team_id: tams.id}}
          = image_tag 'star-yellow.png'
.hidden.in-active-star{id: "in-active-star#{teams.id}", data: {team_id: teams.id}}
          = image_tag 'star-white.png'
css
.hidden {
  display: none;
}

jQuery

本題のいいね機能の実装部分です。
記述が冗長になってしまったので、コメントを入れております。重要なのはcsrfTokenをリクエスト時に持たせることです。これをしないと422 (Unprocessable Entity)というエラーが起きます。なぜかというとGETと違ってPOSTなどの処理はデータベースの変更をするリクエストなので、簡単に操作されては困るため制約がついています。そのため、rails-ujsを使ってaxiosでリクエスト時にcsrfTokenというのを持たせるようにしております。

jQuery
import $ from 'jquery'
import axios from 'axios'

import { csrfToken } from 'rails-ujs'
// リクエスト時にCSRFトークンを持たせる
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()

document.addEventListener('DOMContentLoaded', () => {

  // ロード時にいいねされていない星を配列で取得
  $('.in-active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    let likeData = $(element).data()
    // カスタムデータからチームIDを取得
    let getId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${getId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const inActiveStatus = response.data.hasLiked
        // falseであればいいねされていない => 白い星を表示するために、'hidden'を取り外す
        if ( inActiveStatus === false ) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // ロード時にいいねされている星を配列で取得
  $('.active-star').each(function (index, element) {
    // テンプレートで記述したカスタムデータを取得
    let likeData = $(element).data()
    // カスタムデータからチームIDを取得
    let getId = likeData.teamId
    // カスタムデータを入れてGETリクエストを送る
    axios.get(`/teams/${getId}/like`)
      // リクエストを送ったらレスポンスが返ってくる
      .then((response) => {
        // responseでrenderされたlikeの状態を取得(true or false)
        const activeStatus = response.data.hasLiked
        // trueであればいいねされている => 黄色い星を表示するために、'hidden'を取り外す
        if ( activeStatus === true) {
          $(element).removeClass('hidden')
        } 
      })
  })

  // #create いいねをつけたいときの処理
  $('.in-active-star').on('click', (e) => {
    e.preventDefault();
    let dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    let teamId = dataset.teamId
    // teamIdを使いPOSTリクエストを送る
    axios.post(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#in-active-star${teamId}`).addClass('hidden');
        $(`#active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })

  })

  // #destroy いいねを外したいときの処理
  $('.active-star').on('click', (e) => {
    e.preventDefault();
    let dataset = $(e.currentTarget).data()
    // クリックした要素のidを取得
    let teamId = dataset.teamId
    // teamIdを使いdeleteメソッドを使う
    axios.delete(`/teams/${teamId}/like`)
    .then((response) => {
      // リクエスト成功なら処理を行う
      if (response.data.status === 'ok') {
        $(`#active-star${teamId}`).addClass('hidden');
        $(`#in-active-star${teamId}`).removeClass('hidden');
      }
    })
    // エラー時の処理
    .catch((e) => {
      window.alert('Error')
      console.log(e)
    })
  })

})

まとめ

  • Ajax処理はデベロッパーツールを使いdebuggerconsole.log()を使い値が取れているか確認をしっかりすると開発が捗る。
  • 詳細ページのように一つしかいいねがないときは意識していなかったが、一覧ページなど複数ある中の一つというように特定したいときは個別のidを付与することで実装できる。
  • POSTやDELETE時にはデータベースの操作をすることからCSRFトークンというものが必要になる。

最後に

今回はjQueryを使って実装しました。Vue.jsの学習も始めたので、生JSやjQueryをもっと理解したら開発に使っていきたいなと思っています。このように実装するのもひとつの方法だと思いますが、他にもたくさんの実装方法があると思います。他のライブラリやフレームワークではどのように実装するのか、また一度書いたコードもリファクタリングを積極的にやっていけたらと思います!

参考文献

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

TypeScriptを最小限の環境構築でブラウザコードとして動かす

Node.js環境でTypeScriptを手軽に動かしたければts-nodeを利用することで簡単に実現できます。
しかし、ブラウザ環境では実行前にコンパイルが必須ですし、工夫しなければ分割されたモジュールの依存を解決できません。

ブラウザ環境で何か軽い検証をしたいときにいちいちWebpackの環境構築などしていられないという人のための記事です。
TypeScriptで記述したコードを最小限の手間で最もシンプルにモジュール分割に対応したブラウザコードとして実行する方法を紹介します。

パッケージは最小限に

最小限がテーマです。typescriptだけを導入しましょう。

npm init
npm install typescript --save

TypeScriptの設定をする

最小限の手間がテーマです。configはテンプレートを活用しましょう。

npx tsc --init

生成されたtsconfig.jsonを下記のように変更してください。
(面倒であればコメント行を消去する必要はありません。)

tsconfig.json
{
  "compilerOptions": {
    "target": "ES5",                          
    "module": "amd",                     
    "outDir": "./dist",                        
    "strict": true,                           
    "esModuleInterop": true,                  
    "skipLibCheck": true,                     
    "forceConsistentCasingInFileNames": true  
  }
}

デフォルトから変更しているのは下記2項目のみです。

    "module": "amd",      // 最重要 ブラウザで実行できる形式                     
    "outDir": "./dist",   // 必須ではありませんがディレクトリ汚さないように

ディレクトリ構成

必要性はありませんが、コンパイル前後のファイルがずらりとrootに並んでいると妙なミスをして無駄な時間を過ごしかねません。
最小限には整理したディレクトリ構成にします。

root/
 ┣ dist/
 │ └ index.html
 ┣ src/
 │ ┣ index.ts
 │ └ util.ts
 ┣ package.json
 ┣ package-lock.json
 └ tsconfig.json

TypeScriptコードの作成

いよいよTypeScriptを書きます。
なんでも良いですが、ここではimport, exportをブラウザコードにコンパイルできていることを確認できるようなコードにします。

util.ts
export function testFunc() {
  window.alert('testFunc');
};
index.ts
import { testFunc } from './util';

(function main(){
  testFunc();
})();

HTMLの作成

記述したtsのコードを実行するためのhtmlファイルを作成します。
Emmetを使えばすぐです。

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title></title>
  </head>
  <body>
    <h1>Minimum ts env</h1>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"
      data-main="./index.js"
      integrity="sha512-c3Nl8+7g4LMSTdrm621y7kf9v3SDPnhxLNhcjFJbKECVnmZHTdo+IRO05sNLTH/D3vA6u1X32ehoLC7WFVdheg=="
      crossorigin="anonymous"
    ></script>
  </body>
</html>

途中のscriptタグはRequireJSをCDNで読み込んでいます。
AMD形式にコンパイルされたjsであればdata-main属性に指定したファイルをエントリポイントとして依存関係を解決してくれます。

cdnjsのWebサイトでRequireJSを検索し、そこで入手したscriptタグをそのままペーストするだけです。
cdnjs

ビルドする

以下のコマンドでtsをコンパイルします。

npx tsc

watchしたければ-wオプションを与えてください。

動作確認

dist/index.htmlをブラウザで開けば動作確認ができます。
image.png

終わりに

tscだけではnode_modulesのパスが解決ができないのでライブラリの利用には工夫が必要です。
ライブラリが使える簡易環境を作るならparcelの活用がオススメです。
(この記事で紹介した手順とさほど変わらない手間でモジュールバンドラーを導入できます)

あくまで簡易なのでできないことは多いですが、サクッと環境作る方法を1つ知っておくだけで色々な検証がやりやすくなります。
自分なりの環境構築を1つ覚えておくと良いでしょう。

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

JavaScript で配列要素の特定プロパティの値を map と reduce で連結する

サンプルコード

// データ
const arr = [
  {foo: 'hello',   bar:   1},
  {foo: 'world',   bar:  20},
  {foo: 'goodbye', bar: 300}
]

// foo の文字列をつなげる
const foo_list = arr.map(x => x.foo).reduce((acc, cur) => acc + ', ' + cur)
console.log(`foo_list = ${foo_list}`)

// bar の値を足す
const bar_total = arr.map(x => x.bar).reduce((acc, cur) => acc + cur)
console.log(`bar_total = ${bar_total}`)

実行結果の例 (Node.js v14.15.1 を使用)。

foo_list = hello, world, goodbye
bar_total = 321

参考資料

Array.prototype.map() - JavaScript | MDN

map() メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します。

Array.prototype.reduce() - JavaScript | MDN

reduce() メソッドは、配列の各要素に対して (引数で与えられた) reducer 関数を実行して、単一の出力値を生成します。

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

なぜ const(定数) で宣言したオブジェクトの中身を書き換えられるのか

今javascriptを勉強しているのだが、その過程で得た知識を共有しようと思う。
(間違っていたら指摘していただけると非常に助かります。)

javascriptを体系的に学ぶ前には そういうもの として逃げていたことなのだが、先程 オブジェクトのコピーの挙動を理解したときにピンと来て理解したものが以下である。

まず、以下のことははご存知のことだと思うが

let myList=[0,1,2,3,4];
let mymyList=myList;
mymyList[0]=100

console.log(myList[0],myList[0])//100 100  ⇐両方100になる

javascriptでは以下のようにすると myList も mymyList も両方中身が書き換わってしまう。

今回のテーマを解説する前に、先にこちらの挙動を解説しておくと、

実はmyList という変数は、少し難しく言うと、'そのオブジェクト自体へのアドレスへの参照'を保持しているのである。

もっと簡単に大雑把に説明すると myList は 0,1,2,3,4 といった小さい箱を包み込んだ大きい箱 のメモリ上での場所を指しているのだ。

そして mymyList に '同じ箱のアドレス'を渡してあげたので myList からでも mymyList からでも同じ要素にアクセスできる のである。

ここで本題に戻るが、 const では "その変数が指す大きな箱のアドレスを変えられなくするもの" である。

つまり、

let box=[0,1,2,3]
box=[9,9,9,9]

のように let を使うと box に別ので確保したメモリのアドレスを新しく格納することはできるが、

const を使うとエラーが起きる。

あくまでもconstで固定されたのは、'大きい箱の場所'であって、大きい箱からアクセスされるところは変えることができるのである。

以上が自分のconstについての理解である。

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

EJSのif文でtrueの時だけ出力する。

やりたいこと

<% const person = { name: 'taro' };%>
<% if (person.name) { %><%= person.name %><% } %>
  • 上記のような値があったときだけ出力する記述をスマートに書きたい。
  • <%が多いので減らしたい。

結論

以下のように書くと完結に書くことができます。

<%= person.name ? person.name : '' %>

三項演算子を使用します。偽値の場合は空文字です。

試したこと

<%= if(person.name) { person.name } %> // エラー
<%= if(person.name) person.name %> // エラー
<% if(person.name) person.name %> // 出力されない

偽値を省略できていないのが心残りですが、
結論の<%= person.name ? person.name : '' %>がシンプル。

他にいい方法があったら教えてください :bow_tone1:

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

javascriptの再代入(=)はただの代入ではない!!!

今、Udemyという学習サイトで購入した動画を見ていて勉強しているのだがなかなか深くて勉強になっている。
ド○トインストールみたいな感じかと思って舐めてたらびっくりした。

さて本題に入るのだが、どうやらjavascriptの代入(=)は代入ではないらしい。

自分はこの記事を書く数日前にjavascriptを始めたのだが、=演算子 を使うと、どうやら数値とか文字列とかのプリミティブ型は代入され、オブジェクト型は代入ではなくて参照らしい、という断片的な知識を持ってこの講座に望んでいた。

ところが、

let a=5;
a=3

とすると 機能としてはただの再代入なのだが裏側は別のメモリのアドレス上に確保された 3 に対して参照を張り替えているらしい。
自分の理解が正しければ、元々あった5というのは3に書き換えられるのではなく、放置され新しいところに別の数値の箱が作られる、ということだ。
日頃からChromeで次々とタブを開く自分に似ていて可愛らしいやつだ(可愛くない)

オブジェクト型の =演算子を使ったときの挙動も理解したが、それは別の機会で。。。

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

DjangoでDBのデータを更新&取得してD3.jsのグラフにする

はじめに

現在,深層学習でリアルタイムに予測したPython(Django)での数値データを,グラフで描画するWebアプリを作っています。

グラフの描画は,matplotlibを用いて画像をHTMLファイルに貼り付けてもいいのですが,そのうちグラフにインタラクティブ性を持たせたかったため,D3.jsを用いてグラフ描画を行います。
D3.jsはJavaScriptのデータから,SVGデータを出力できるライブラリです。

今回作っているアプリでは,1分間に1度,リアルタイムに予測された数値をデータベースに保存し,それらのデータを時系列データとして,折れ線グラフで表示することを行います。

サンプルプログラム

ファイル構造

「system」というプロジェクトを作成
「template」の中のindex.htmlを最終的に作ります(今回はtemplateの機能は使っていませんが...)

.
├── mysite
│   ├── ....
│   ├── ....
│   └── setting.py *
│
├── templates
│   └── index.html *
│
├── system             <-プロジェクト名
│   ├── views.py *
│   └── urls.py *
│
├── manage.py
└── ◯◯◯◯.db            <-データが保存されるDB

この中で,*が付いているものを説明していきます。

データベースの設定・構造

深層学習のモデルから得られた出力(0から1の小数)をpredとしてDBに保存するため,そのためのDB設定を行います。

設定

setting.pyでSQLiteのdbファイルを設定します

mysite/setting.py
...

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, '◯◯◯◯.sqlite3'),
    }
}

...

DBの構造

カラム名 id pred
int real
説明 データのID 予測値

実際にテーブルを作るSQLを書くとしたらこんな感じ↓(Djangoなので,実際にはコマンドを叩くことはないです)
テーブル名は{プロジェクト名}_{テーブルのモデル名}になる。

CREATE TABLE "system_preddata" (
    "id"    integer NOT NULL,
    "pred"  real NOT NULL,
    PRIMARY KEY("id" AUTOINCREMENT)
);

Python側の処理

pred_func()は深層学習モデルがある値の出力をする,架空の関数です。

system/views.py
from django.db import models

# テーブルのモデルを指定
class PredData(models.Model):
    pred = models.FloatField()
    def __str__(self):
        return '<pred:'+str(self.pred)+'>'

# 新しいデータを加える
def data_insert(insert_data):
    d = insert_data
    t = PredData(pred = d[0])
    t.save()

# 予測されたデータをDBに加えて,データを全て取得し,index.htmlに返す
def main(request):
    red = pred_func() #予測する関数
    insert_data = [pred]
    data_insert(insert_data) #データを追加する
    d = {'pred_data': PredData.objects.all()} #データを全て取得する
    return render(request, 'index.html', d)

system/url.py
from django.conf.urls import url
from . import views

# views.pred_systemの関数を呼び出す
urlpatterns = [
    ...
    url(r'^$', views. main, name='main'),
]

HTML側の処理

/system/index.htmlの中身です。

D3.jsの部分は,こちらのサイトを参考にしました
https://wizardace.com/d3-linechart-base/

グラフでの

templates/index.html
<!DOCTYPE html>
<html>
<head lang="ja">
  <meta charset="UTF-8">

  <!-- 60秒後に更新 -->
  <meta http-equiv="refresh" content="60">

  <!-- D3.jsの読み込み -->
  <script src="https://d3js.org/d3.v5.min.js"></script>   
  <title></title>
</head>
<body>

<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script type="text/javascript">

  // データセットをつくる
  var data = [] //深層学習による予測値
  var minutes_len = 0 //データの個数をカウント
  {% for d in pred_data %}
    data.push(["{{ d.id }}"-1,"{{ d.pred }}"]);
    minutes_len ++;
  {% endfor %}
  console.log(data);
  console.log(minutes_len);

  var width = 800; // グラフの幅
  var height = 400; // グラフの高さ
  var margin = { "top": 30, "bottom": 60, "right": 30, "left": 60 };

  // SVG領域の設定
  var svg = d3.select("body").append("svg").attr("width", width).attr("height", height);

  // 軸の長さ
  var xScale = d3.scaleLinear()
    .domain([0, minutes_len]) // x軸はデータの数によって決める
    .range([margin.left, width - margin.right]);

  var yScale = d3.scaleLinear()
    .domain([0,1]) //0<予測値<1
    .range([height - margin.bottom, margin.top]);

  // 軸の表示
  var axisx = d3.axisBottom(xScale).ticks(minutes_len);
  var axisy = d3.axisLeft(yScale).ticks(5);

  svg.append("g")
    .attr("transform", "translate(" + 0 + "," + (height - margin.bottom) + ")")
    .call(axisx)
    .append("text")
    .attr("fill", "black")
    .attr("x", (width - margin.left - margin.right) / 2 + margin.left)
    .attr("y", 35)
    .attr("text-anchor", "middle")
    .attr("font-size", "10pt")
    .attr("font-weight", "bold")
    .text("X Label");

  svg.append("g")
    .attr("transform", "translate(" + margin.left + "," + 0 + ")")
    .call(axisy)
    .append("text")
    .attr("fill", "black")
    .attr("text-anchor", "middle")
    .attr("x", -(height - margin.top - margin.bottom) / 2 - margin.top)
    .attr("y", -35)
    .attr("transform", "rotate(-90)")
    .attr("font-weight", "bold")
    .attr("font-size", "10pt")
    .text("Y Label");

  // グラフのラインを表示
  svg.append("path")
    .datum(data)
    .attr("fill", "none")
    .attr("stroke", "blue")
    .attr("stroke-width", 3)
    .attr("d", d3.line()
      .x(function(d) { return xScale(d[0]); })
      .y(function(d) { return yScale(d[1]); }));
</script>

<br>

<!-- 実際の値をテーブルで表示をして確認 -->
<table border="1">
  <thead>
    <tr>
      <th>id</th>
      <th>pred</th>
    </tr>
  </thead>
  <tbody>
    {% for d in pred_data %}
      <tr>
        <td>{{ d.id }}</td>
        <td>{{ d.pred }}</td>
      </tr>
    {% endfor %}
  </tbody>
</table>

</body>
</html>

グラフはこのようになります↓
スクリーンショット 2020-10-18 22.32.03.png

まとめ

D3.jsとDjangoを連携する方法について書きました。
これから,インタラクティブなグラフになる仕組みを作っていけたらと思います。

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

Intl.DateTimeFormatを使った日付や時間の0詰め

本題

最近仕事でNode.jsとV8のチェンジログを追っていたところ、Intl.DateTimeFormatという新機能を見つけました。使うと、日付や時間の0詰めを外部ライブラリに頼らずにシュッと書けそうなので試してみました。

サンプルコードは以下のような感じです。

const date = new Date('2020-01-01');
const dateStr =new Intl.DateTimeFormat('jp', {
  year: 'numeric', month: '2-digit', day: '2-digit',
}).format(date);
console.log(dateStr); // 2020/01/01
const date2 = new Date('2020-01-01');
const dateStr2 = new Intl.DateTimeFormat('jp', options = {
  year: 'numeric', month: '2-digit', day: '2-digit', 
  hour: '2-digit', minute: '2-digit', second: '2-digit', 
}).format(date2);
console.log(dateStr2); // 2020/01/01 09:00:00

コンストラクタIntl.DateTimeFormatの第2引数にはオブジェクトで表示のオプションが渡せます。プロパティ値が2-digitなら0詰めした値numericならそのままの値でフォーマットします。第1引数にはロケールを渡すのですが、各国の主要な表示方法でフォーマットするようです。

これで以下のようなコードとはおさばらじゃ!!

const date3 = new Date('2020-01-01');
const month = ('0' + (date3.getMonth()+1)).slice(-2);

参考

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

IE11対応(JavaScript)

IE対応の意義

下記URLを見ると2020年11月現在でも未だIE対応しなければならない現実があります.
https://gs.statcounter.com/browser-market-share/desktop/japan/#monthly-201810-202010

下記の通り2020年11月現在, IEが7.88%のシェアを占めています.
IE_adjustment01.JPG

(ちなみに全世界だとIEのシェアは2.15%まで落ちます(2020年11月現在))

世の中ではまだまだIEが使われていることがわかる.

IE11においては, JavaScript?のES6が一部のみの対応に留まっている.
モダンな実装ができないと困るのでBabelというものを使う.

Babel

JavaScript?のコンパイラ. ES6以降の構文をES5以前の形式に変換するのに利用されたり, TypeScript?のコンパイルにも使われたりする.
https://babeljs.io

例えば下記のコードは普通だとIE11では動きません
アロー関数とテンプレートリテラルがIE11で非対応だからです.

ES6
//Doesnot work on IE11  
let foobar = ( hoge, fuga ) => {  
        return console.log("ほげの出力:${hoge}");  
//~~~~~~~~~~処理~~~~~~~~~~~~~~  
        return console.log("ふがの出力:${fuga}");  
//~~~~~~~~~~処理~~~~~~~~~~~~~~  
  }  

これを下記のようなES5形式に自動コンパイルしてくれる.

ES5
// Works on IE11  
let hogefuga = function( hoge, fuga ){  
        return console.log("ほげの出力:" + hoge);  
//~~~~~~~~~~処理~~~~~~~~~~~~~~  
        return console.log("ふがの出力:" + fuga);  
//~~~~~~~~~~処理~~~~~~~~~~~~~~  
  }  

ES6形式の方が効率がよく, できることも多く, 実装によっては(promiseなどは特に)必須になってくることがあるので,
コンパイルしてもらったほうがいい.

BabelPolyfill

スタンドアロンのBabelPolyfill?を使うとIE11でもES6記法が動く.
CDNだけで対応できてお手軽.

BabelスタンドアロンCDNの場合

html
<script src="https://cdn.jsdelivr.net/npm/@babel/polyfill@latest/dist/polyfill.min.js"></script>  
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@latest/babel.min.js"></script>  
<script type="text/babel" data-presets="env,stage-3" src="./js/main.js"></script>  

main.jsは適宜書き換えてください.

npmの場合

npm install --save @babel/polyfill  
import "@babel/polyfill";
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript:変数・文字列の取得

全く、どういう時に使うのかイメージできずにやった事と調べたことを煩雑に書きなぐりながらどんどん進む内容です。

本日の学習

インクリメント演算子とデクリメント演算子

インクリメント

a++; // ++演算子で1増加

デクリメントは逆

a--; // --演算子で1減少

使いどころはまだ分からない。

変数を使ったあれこれ

代入

// 変数a2に変数aの値を2倍した結果を代入
let z = 8;
let z2 = 8 * 2;

console.log("2倍した数:" + z2);
// 変数yomiに変数yの値を代入

let y = "yen";
yomi = y;

console.log("yomi=" + yomi);

変数の入れ替え

// 変数aと変数bの値を入れ替えてください

let a = 5;
let b = 23;

let tmp = b;
b = a;
a = tmp;

console.log("a = " + a + ", b = " + b);

なんでこんな面倒くさい事するんだろう、と思ったら、一回別のところに値を移動しないと入れ替えができないらしい。入れ替えるのも一苦労。

部分文字の取得

// 文字列sの左から3つ目の文字を出力してください

let s = "ranbo";

console.log(s[3-1]); 

部分文字列の取得

substring

let a = "ranbosan"
console.log(a.substring(2));

substringメソッドっていうらしい
引数の「開始位置」から最後の文字までを返す。
最初の1文字目の位置は0から始まるので、1引いている。

let a = "ranbosan"
console.log(a.substring(2,6));

こう書くと、1つめの引数(開始位置)から2つめの引数(終了位置)の前にある文字までを返すらしい。

// 文字列sの左から3個目、そこから3文字分の文字列を出力。

let s = "ことばあそびはおもしろい";
console.log(s.substring(4-1,4-1+3));

n個目の文字からm文字分の部分文字列はs.substring(n-1, n-1+m)で取得
ここでも左端は0なので、n個目の文字+1を入れる

部分文字列の検索

indexOf

// 文字列sの中で文字列"taxi"が出現する位置を出力。一番左の文字の位置は0

let s = "aaaaaaaaaaahei!taxiyyyyyyyyyyy";
let index = s.indexOf("taxi") + 1;
console.log(index + "文字目から始まる");

対象となる文字列の後ろに「.」ピリオドを付けて記載。

文字列の長さ

length

// 文字列sの長さを出力してください

let s = "Ability will never catch up with the demand for it.";
console.log(s.length + "文字使っています");

スペースは含んでいるみたい。
本日はここまで。

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

IE11対応【JavaScript編】

IE11対応の意義

下記URLを見るとレガシーと言われながらも2020年11月現在でも未だIE対応しなければならない現実があります.
https://gs.statcounter.com/browser-market-share/desktop/japan/#monthly-201810-202010
下記の通り2020年11月現在, デスクトップではIEが7.88%のシェアを占めています.

browser_share_jp_01.JPG
(ちなみに全世界だと全デスクトップブラウザのうちのIEのシェアは2.15%まで落ちます(2020年11月現在))

世の中ではまだまだIEが使われていることがわかる.

IE11においては, JavaScriptのES6が一部のみの対応に留まっている.
モダンな実装ができないと困るのでBabelというものを使う.

Babel

JavaScriptのコンパイラ. ES6以降の構文をES5以前の形式に変換するのに利用されたり, TypeScriptのコンパイルにも使われたりする.

https://babeljs.io/

例えば下記のコードは普通だとIE11では動きません
アロー関数がIE11で非対応だからです.

let hogefuga = ( hoge, fuga ) => {
        return console.log("ほげの出力:${hoge}");
//~~~~~~~~~~処理~~~~~~~~~~~~~~
        return console.log("ふがの出力:${fuga}");
//~~~~~~~~~~処理~~~~~~~~~~~~~~
  }

これを下記のようにES5形式に自動的にコンパイルしてくれるのがBabel.

let hogefuga = function( hoge, fuga ){
        return console.log("ほげの出力:" + hoge);
//~~~~~~~~~~処理~~~~~~~~~~~~~~
        return console.log("ふがの出力:" + fuga);
//~~~~~~~~~~処理~~~~~~~~~~~~~~
  }

ES6形式の方が効率がよく, できることも多く, 実装によっては(promiseなどは特に)必須になってくることがあるので,
コンパイルしてもらったほうがいい.

Babel Polyfill

Polyfillを使うといろんなブラウザ(IE11)でもES6記法が動くようになる

import "@babel/polyfill";

npmの場合

npm install --save @babel/polyfill

yarnの場合

yarn add @babel/polyfill

スタンドアロンCDNの場合

ES6記法を使う直前に以下を記述.

<script src="https://cdn.jsdelivr.net/npm/@babel/polyfill@latest/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@latest/babel.min.js"></script>
<script type="text/babel" data-presets="env,stage-3" src="./js/main.js"></script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React+Firestoreでブックマーク(お気に入り)の保存機能を作成する

現在ReactとFirestoreで作成しているブログのようなwebサービスの各投稿をブックマークして、別ページで閲覧できるようにするような機能を追加したので、その解説をします。

前提

解説をする上で、前提があります。

  • ReactのFunction ComponentやHooks、contextをなんとなくでも使える段階にある
  • Firestoreのcollectionやdocument、subcollectionについてそれらがどういうものかぼんやりとでも知っている
  • Firestoreのデータを読み込んだり書き込んだりできる

上記の二つが前提となる知識です。

さらにこの記事では大まかな実装の流れや考え方を解説するのでコードを省略している部分もあります
この記事だけで実装はできませんが、ブックマーク機能やお気に入り機能を実装する上でのヒントになると思っています。(人それぞれコードや実装の方法が違いますからね?)

機能について

今回はブックマーク機能を実装すると言いましたが、どのようなものなのかは以下の画像をご覧ください。

スクリーンショット 2020-11-25 16.06.58.png

各投稿に対し、右下にブックマークのiconを表示します。
このiconをクリックし、ブックマーク登録がされると色がつき、もう一度クリックすると枠だけのiconに戻ります。

このブックマークのiconにはmaterial uiのiconを使います。

Material UIの導入についてはこちらの記事が参考になると思います。

また、保存した記事は画像の上部のheaderにある紙のようなiconをクリックすることで保存した記事を一覧表示するページに飛べます。

実装

さて、早速実装していきましょう。

db

まず、Firestoreのdb設計についてですが、usersコレクションの中にある各ユーザーに対して、サブコレクションとしてbookmarksという名前のものを追加し、そこにブックマークした記事を保存していきます。

usersコレクション→それぞれのuserのドキュメント→bookmarksというサブコレクション→bookmarksのドキュメント

users - userID - bookmarks - document - id
                                      - post_id
                                      - authoName
                                      - createdAt
                                      - title     

投稿に対して、ブックマークを押したら現在ログインしているユーザーのデータに、ブックマークした記事のデータが保存される感じです。

一覧画面でのブックマーク

今回は、各投稿に対してブックマークボタンを配置しているので、投稿のcardを生成しているコンポーネントにブックマークのiconを追加する必要があります。

僕の場合はPostコンポーネントをforEachで回して記事を表示しているので、Postコンポーネントにiconを追加します。

Post.jsx
//importやstyle含め、いろいろ省略しています。

const Post = ({ authorName, content, createdAt, title, id, uid}) => {
  const { currentUser, savePostToBookmark, removePostFromBookmark } = useAuth();
  const [saved, setSaved] = useState(false)

  //ここからが該当の場所です!!
  const savePost = () => {
    setSaved(true)
    const savedPosts = ({ authorName, content, createdAt, title, id});
    return savePostToBookmark(savedPosts)
  }

  const removeBookmark = async (id) => {
    setSaved(false)
    removePostFromBookmark(id)
  };

  //useEffect内では、ログアウトして再度ログインした際や、画面を再読み込みした際などにも
  //ブックマークした投稿が消えないようにするために、ログインユーザーのサブコレクションbookmarksを参照し、
  //もしpostのidと保存された投稿のpostのidが等しければsaved=trueとすることでブックマークされた状態を表示するようにしています。
  useEffect(() => {
    const uid = currentUser.uid
    db.collection('users').doc(uid).get()
    .then(doc => {
        if (doc.exists) {
          db.collection('users').doc(uid).collection('bookmarks').get()
          .then(snapshots => {
            snapshots.docs.forEach(doc => {
              const data = doc.data();
              // saveIdはbookmarkしたpostのid
              const saveId = data.id
              if (saveId === id) setSaved(true)
            })
          })
        }
      })

  }, [])
  //ここまで

    return (
        <>
        <Card className={classes.root} variant="outlined">
            <CardContent style={{ paddingBottom: "0" }}>           
            {/*ここからが該当の場所です!!*/}
            {saved === true ? 
            <IconButton className={classes.likeBtn} onClick={removeBookmark}>
              <BookmarkIcon />
            </IconButton>
            :
            <IconButton className={classes.likeBtn} onClick={savePost}>
               <BookmarkBorderIcon />
            </IconButton>
            }
            {/*ここまで*/}
        </Card>
        </>
    )
}

ブックマークされている状態のときは、<BookmarkIcon />
ブックマークされていnaい状態のときは、< BookmarkBorderIcon />
を表示します。

では、どのようにしてブックマークされているのかどうかを判断しているのかというと、savedというstateのtrueかfalseによって判断しています。const [saved, setSaved] = useState(false)の部分で定義しています。初期値、つまり何もしていない状態ではもちろんブックマークは登録されていないので、savedの初期値はfalseを入れています。ブックマークが登録されたタイミングで、savedがtrueとなり、iconに色がつきます(正確には塗り潰されたiconに入れ替わる)。

ブックマーク登録をし、dbにデータを追加する

ブックマークが登録されていない状態で、iconをクリックすると、savePostという関数が走ります。
この関数ではブックマークを押した投稿のデータをdbに保存します。もう一度コードを記載します。

Post.jsx
 //クリックされたらこの関数が走る
  const savePost = () => {
    //まずはsavedをtrueに
    setSaved(true)
    //savedPostsはクリックされたiconの投稿の情報
    const savedPosts = ({ authorName, content, createdAt, title, id});
    //savedPostsをsavePostToBookmarkに渡して実行
    return savePostToBookmark(savedPosts)
  }

ここでのsavePostToBookmarkは、contextを用いて別のファイルで書いた関数です。内容は以下の通り。

context.jsx
  const savePostToBookmark = async(savedPosts) => {
    const uid = currentUser.uid
    //saveRefではサブコレクションbookmarksを参照し、
    const saveRef = db.collection('users').doc(uid).collection('bookmarks').doc();
    //以下のコードで、savedPostsに自身のid(bookmarksのdocument id)を追加します。
    savedPosts['saveId'] = saveRef.id;
    //Post.jsxから渡ってきた情報(savedPosts)を保存します。
    await saveRef.set(savedPosts);
  }

これにてブックマークの登録は完了です。

ブックマーク登録を解除し、dbからデータを削除する

ここでは削除機能を実装します。

Post.jsx
  const removeBookmark = async (id) => {
    //まずはsavedをtrueに
    setSaved(false)
    //contextの関数にデータを渡す
    removePostFromBookmark(id)
  };

ここでも、removePostFromBookmarkという関数をcontextから呼び出して使っています。

context.jsx
  const removePostFromBookmark = id => {
    const uid = currentUser.uid
    //Post.jsxから渡ってきたデータ(今回はid)を元にdbから該当のデータを探し出し、delete()でデータを削除
    db.collection('users').doc(uid)
        .collection('bookmarks').doc(id)
        .delete();
  };

ブックマーク済みのiconをクリックするとremoveBookmarkが実行され、ブックマークのデータがdbから削除されます。この時にsetSaved(false)でsavedをfalseとして表示されるbookmarkのiconを切り替えます。

以上がブックマークの登録と削除でした。

ブックマークした投稿を別ページで表示しよう

ここでは、ブックマークした投稿を別のページで一覧表示する機能と、その画面でブックマークを解除した時に一覧から投稿が消えるような機能を実装します。

スクリーンショット 2020-11-25 17.46.21.png

ブックマークされた投稿を取得し表示する

ブックマークされた投稿を表示します。各投稿は子のコンポーネントとして別でコンポーネントを作成し、mapで回して表示します。(ブックマークした投稿が一つだけでも配列として保存しているのでmapかforEachする必要があります)

BookmarkList.jsx
export default function BookmarkList() {
    const { currentUser } = useAuth()
    //dbから取り出した各bookamarkのデータを保持するためのstateを用意する
    const [bookmarks, setBookmarks] = useState([])

    useEffect(() => {
        const uid = currentUser.uid;
        let posts = [];
        //サブコレクションbookmarksを取得し
        db.collection('users').doc(uid).collection('bookmarks').get()
            .then(snapshots => {
                //forEachで中身を取り出す。
                snapshots.docs.forEach(doc => {
                    const data = doc.data()
                    //取り出したデータをpostsという配列にpush
                    posts.push({
                        authorName: data.authorName,
                        content: data.content,
                        createdAt: data.createdAt,
                        title: data.title,
                        saveId: doc.id,
                        uid: data.uid,
                        post_id: data.id
                    })

                })
                //用意したbookmarksにstateでpostsのデータを保持する
                setBookmarks(posts)
            })
    }, [])

    return (
        <>
            <div style={{ marginTop: "100px" }}>
            <h3>保存した投稿</h3>
            {/*stateのbookmarksはforEachで回した分データを配列として持っているので、mapで取り出す*/}
            {bookmarks.map(bookmark => 
               {/*BookmarkListItemにデータを渡す*/}
                <BookmarkListItem 
                    key={bookmark.saveId}
                    authorName={bookmark.authorName}
                    content={bookmark.content}
                    createdAt={bookmark.createdAt}
                    title={bookmark.title}
                    // bookmarkのid
                    id={bookmark.saveId}
                    post_id={bookmark.post_id}
                />
            )}
            </div>
        </>
    )
}

続いて、BookmarkListItemです。
既にブックマークをされている投稿を表示しているので、ブックマークに追加するためのiconや関数は用意していません。

BookmarkListItem.jsx
export default function BookmarkListItem({ authorName, content, createdAt, title, post_id, id}) {
    const { removePostFromBookmark } = useAuth()
    const [saved, setSaved] = useState(true)

    //ここではcontextから読み込んだブックマークを解除する関数を実行するためのコードを書きます。
    const removeBookmark = () => {
        //まずはsavedをfalseに
        setSaved(false)
        //contextから読み込んだremovePostFromBookmarkにidを渡し、ブックマークのデータをdbから削除
        removePostFromBookmark(id)
    };

    return (
        <>
        {/*このコンポーネントはブックマーク登録されていないものは表示する必要がないの*/}
        {/*そこで全体を条件式で囲ってsaved=trueの場合(つまり、ブックマークされている場合)のみ表示するようにしている*/}
        {saved === true && 
        <Card className={classes.root} variant="outlined">
            <CardContent style={{ paddingBottom: "0" }}>
                <Typography variant="h5" component="h3">
                  {title}
                </Typography>
                <Typography className={classes.pos} color="textSecondary">
                  {authorName+""+createdAt}
                </Typography>
                <Typography className={classes.contentText} variant="body2" component="p">
                  {content}
                </Typography>
            </CardContent>
            <CardActions className={classes.detailBtnWrap}>
              <Link to={'/detail/' + post_id} className={classes.detailLink}>
                <Button variant="contained" className={classes.detailButton} size="small">詳細を表示</Button>
              </Link>
            </CardActions>
            <IconButton className={classes.likeBtn} onClick={() => removeBookmark(id)}>
               <BookmarkIcon />
            </IconButton>
        </Card>
        }
        </>
    )
}

こんな感じで実装は完了です。

最後に

いろいろ省略して解説してきましたが、ブックマークやお気に入りしてそれらを一覧表示したいという方の役に立てたら幸いです。

また、「ここはこういう書き方の方がいい」みたいなコードの提案や、間違ってる部分の指摘など、コメントでお待ちしております。

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

[ReactNative] ScreenView+Webview refreshControlについての備忘録(Androidのバグ)

最近小さめのReactNativeのプロジェクトにアサインされたワタクシでございます。
全くまだ分かってないんですけど、UI部分の改修案件が来たので対応しています。
その際に直面したバグ対応の備忘録を残します。

どのようなバグか

iosでは起きず、Androidのみのバグとなります。

render() {
  <ScrollView
     refreshControl={
       <RefreshControl
         refreshing={ this.state.refreshing }
         style={{ backgroundColor: "#FFFFFF" }}
         onRefresh={() => { this.onRefresh() }}
       />
     }
     contentContainerStyle={{flex: 1}}
     style={{ backgroundColor: "#FFFFFF" }}
   >
            <WebView
              style={{ flex: 1 }}
              onMessage={this.onMessage}
              onNavigationStateChange={this.onNavigationStateChange}
              onLoadStart={this.onLoadStart}
              onLoadEnd={this.onLoadEnd}
              ref={webView => {this.webView = webView}}
              source={{ uri: this.state.url.toString() }}
            />
    </ScrollView>
}

このようなソースがあります。
webviewを表示しており、このwebviewはスクロールが可能になっています。
スクロールをする際にiosでは起きませんが、Androidではスクロールで下までいってしまったあとに上に戻ろうとすると必ずrefreshControlが走り、上にスクロールが出来なくなるバグが起きてました。
アプリの上部に来た時だけ更新してくれればいいのに、常に上部をひっぱると更新されてしまいます。

原因

webview側は問題なくスクロールします。スクロールされているのはScrollViewではなくて、webviewであることがわかり、(ScrollViewにonScrollつけてもイベントが走らなかったため)ScrollViewのscrollYが更新されず0のままでした。
そうすると恐らく、scrollYが0であるとしてAndroidでは常にrefreshControlが走ってしまうのではないか、というところまでたどり着きました。

開発環境

  • react-native: 0.61.5
  • react: 16.9.5
  • react-native-webview: 8.0.3

解決方法

const INJECTED_JS = `
  window.onscroll = function() {
    window.ReactNativeWebView.postMessage(
      JSON.stringify({
        scrollTop: document.documentElement.scrollTop || document.body.scrollTop
      }),
    )
`

export default class WebViewPage extends React.Component {
  state = {
    scrollViewHeight: 0,
    isPullToRefreshEnabled: false,
  }

  onMessage = () => {
    const { data } = e.nativeEvent

    try {
      const { scrollTop } = JSON.parse(data)
      this.setState({ isPullToRefreshEnabled: scrollTop === 0 })
    } catch (err) {
      // ...
    }
  }

  render () {
    const { scrollViewHeight, isPullToRefreshEnabled } = this.state

    return (
      <View style={{flex:1}}>
        <ScrollView
          refreshControl={
            <RefreshControl
              refreshing={ this.state.refreshing }
              enabled={ isPullToRefreshEnabled }
              style={{ backgroundColor: "#FFFFFF" }}
              onRefresh={() => { this.onRefresh() }}
            />
          }
          onLayout={e => this.setState({ scrollViewHeight: e.nativeEvent.layout.height })}
          contentContainerStyle={{flex: 1}}
          style={{ backgroundColor: "#FFFFFF" }}
        >
          <WebView
            style={{ flex: 1 }}
            onMessage={this.onMessage}
            onNavigationStateChange={this.onNavigationStateChange}
            onLoadStart={this.onLoadStart}
            onLoadEnd={this.onLoadEnd}
            ref={webView => {this.webView = webView}}
            source={{ uri: this.state.url.toString() }}
            injectedJavaScript={INJECTED_JS}
          />
        </ScrollView>
      </View>
    )
  }
}

解説

  1. ScrollViewでスクロールイベント取れないならwebviewで取るしかないよなということで injectedJavaScript を導入。injectedJavaScriptはwebview側にjavascriptのコードを渡す属性で、ここでwebviewのスクロール位置を取得して、window.ReactNativeWebView.postMessage でReactNative側にその情報を渡してやります。この時、webview側にもonMessageメソッドを定義して、webview側のスクロール位置情報を受け取ります
  2. enabledでいつrefreshControlが走るか定義しておく(onMessage)
  3. ScrollViewコンポーネントのonLayoutを使って、viewが開いたときの高さを取得して保持しておきます。(初回だけ取得用)
  4. WebviewコンポーネントにinjectedJavaScriptを突っ込みます。

以上です。これで画面の一番上にスクロールした時にだけ上部を引っ張ればrefreshControlが走る設計になったかと思います。

参考文献

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

GoToイート加盟店舗を地図上で選べるサービス作った

こんなもの

・「食べログ」「ぐるなび」「ホットペッパー」を統括してGoToイートの店舗を確認できるブラウザアプリ
・ 地図ベースで手軽と検索できる(個人店舗を応援するためのもので、有名チェーン店は除外)
・ 煩雑なインストールなし(PWAは対応)、ユーザ登録なし
幹事マップはこちら

コンテンツ

スマホでの表示画面
Image from iOS.png

マーカクリックで店舗情報が表示されます。
Image from iOS (3).png

ぐるなびで検索した場合で、検索に駅名を入力するとそこへ飛べます。
Image from iOS (2).png

ホットペッパーで検索した場合
Image from iOS (5).png

利用技術一覧

・javascript
・OpenStreetMap + Leaflet
・jquery
・MySQL
・PWA
・PHP(軽量化のためフルスクラッチ)
・GPS

特徴

・ズームレベルで周辺店舗を統括して見やすさ向上
・非同期通信によるUX向上
・逐次通信によるデータ転送量の削減
・タイルサーバは自前
・PWA化対応

気になること

・トラフィックが増えたときの挙動
・デザインが受け入れられるのかどうか
・GoToイート終わりそうw

最後に

もしよかったら触ってみてフィードバックを頂ければ大変嬉しく思います。どうぞよろしくお願いいたします。
https://kanjimap.jp

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

子供向けの本でチャット風クイズゲームを作ってみた

最近やっとプログラミングがちょっとだけわかってきたので、
今まで苦手意識が強かったJavaScriptと向き合ってみようと思う。

楽しくできてわかりやすい物がいいなと思ったので、 子供向けの本買ってやってみた。

語学も絵本とかで勉強するといいって聞くし、人は成功体験が大切だし。

チャット風クイズゲームを作ってみよう

本は動物の鳴き声が出されるので英語で動物の名前を答える作りなので、
そこは変えて世界遺産の問題にした。

quiz.html
<DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>世界遺産一問一答</title>
    <link rel="stylesheet" href="css/quiz.css">
  </head>
  <body>
    <div class="chat-quiz">
      <h2>世界遺産一問一答</h2>
      <ul id="chat">
        <li>世界遺産の問題を出すよ!それでは問題です!!!</li>
        <li>ででん!</li>
      </ul>
      <input type="text">
      <button type="button">答え</button>
    </div>
    <script src="js/quizdata.js"></script>
    <script src="js/quiz.js"></script>
  </body>
</html>

吹き出しは本に付いていた画像を使用しています。
oddが奇数で出題者、evenが偶数で回答者です。
一問一答なので、連投はできません。

quiz.css
.chat-quiz {
 width: 600px;
 margin: auto;
}

input {
 margin-left: 140px;
}

li:nth-child(odd) {
 background: url("../img/girl.png");
 width: 380px;
 height: 21px;
 padding: 20px 110px;
 line-height: 13pt;
 list-style: none;
}

li:nth-child(even) {
 background: url("../img/boy.png");
 width: 380px;
 height: 21px;
 padding: 20px 110px;
 line-height: 13pt;
 list-style: none;
}

慣れないぜ、JavaScript

quizdata.jsでは問題と答えの配列を作ります。
できるまで次の問題になりません。

quizdata.js
var quiz = [
 ['自由の女神はどこの国にあるでしょう?', 'アメリカ'],
 ['厳島神社に何県にあるでしょう?', '鹿児島'],
 ['モアイ像はどこの国にあるでしょう?', 'チリ'],
 ['姫路城に何県にあるでしょう?', '兵庫'],
 ['ナスカの地上絵はどこの国にあるでしょう?', 'ペルー'],
];

quiz.jsで処理を書いてます。
要素とかid取ってくるとjsやってる感じがする。

quiz.js
var addMessage = (mes) => {
  // ul要素を取得
  var ul = document.getElementById('chat');
  // li要素を作成
  var li = document.createElement('li');
  // テキスト情報を作成
  var text = document.createTextNode(mes);

  // ul要素に追加
  li.appendChild(text);
  ul.appendChild(li);
}

// 問題追加
var qnum = 0;
addMessage(quiz[qnum][0]);

var btn = document.querySelector('button');
btn.addEventListener('click', () => {
  var ipt = document.querySelector('input');
  addMessage(ipt.value);
  if (ipt.value == quiz[qnum][1]) {
    qnum += 1;
    // 最後の問題の後は最初の問題に戻る
    qnum %= quiz.length;
    addMessage('正解!' + quiz[qnum][0]);
  } else {
    addMessage('違うよ〜');
  }
});

確認してみよう

トップ画面。
問題が出題されています。

スクリーンショット 2020-12-14 1.58.46.png

不正解の答えを入力してみます。
正解するまでこの問題です。パスはなし。
スクリーンショット 2020-12-14 18.32.23.png

正解の答えを入力してみます。
正解したら次の問題がすぐに表示されます。
スクリーンショット 2020-12-14 18.35.31.png

所感

苦手意識が強い人には子供向けの本は良いと思った。
少し苦手意識が減ったので良かった。

もっとスマートな書き方があるんだろうなと思ったけど、
最初は愚直にやっていくのが一番だと思っているのでちょうどいいと思う。

お読みいただきましてありがとうございました。

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

【受験予定者向け、2020年11月時点】CIW Javascript Specialist資格試験に落ちました。

tltr

Web開発経験1.5年とJS入門参考書2回読破で試験に挑んで、66/100で75点合格ラインに届かず落ちました。
公式の出題範囲は一般的なJS入門参考書の内容だと書いてありますが、実際の問題では2/3はCookieに関するマニアックな問題でした。落ちた原因は設問範囲が偏っていることだと考えています。運が悪くて範囲がおかしい問題セットに遭遇した可能性もあります。

試験自体

1D0-435 CIW Javascript Specialist (Japanese Only)を受けました。英語版の試験は1D0-735(2020年11月時点の番号です。番号は毎年変わる?)です。
出題範囲はJSの入門レベルの内容:文法、データ型、よく使うオブジェクトと関数、ブラウザDOM、Form、JQuery、AJAX、Web Storage API、Geo API、Canvas API
受験費は165ドルで約16500円です。
時間は90分で、50問の4択問題で、75点で合格です。

公式:https://www.ciwcertified.com/ciw-certifications/web-development-series/javascript-specialist

受験前

公式テキストと過去問は15000円で値段が高くて、それを買いませんでした。
公式以外の6000円くらいの過去問も解きました。公式サイトに書いてある出題範囲と同じく、JSの入門レベルの内容です(実際の出題範囲は全然違いましたが)。

受験後

66/100点で75点合格ラインに届かずに落ちました。
運が悪いせいか、問題の2/3がCookieに関するマニアックな問題でした(e.g., Cookieの保存場所、Cookie複数サイト共有方法。JS入門書ではCookieをそこまで解説していません。CIW Cookie Specialist説)。実際の開発でもそんなにcookieを触っていません。検証したことないですが、1D0-435 Japanese Onlyだけ出題範囲がおかしくて、英語版の1D0-735は大丈夫かもしれません。

文法、関数、データ型、HTMLに関する問題は少ないです(8問くらい)。印象に残った問題は、HTMLのttタグを生成するjavascriptコードの問題で、そもそもttタグはHTML5で廃止されますし、実際の開発で一回も使ったことがないです。

日本語がおかしい問題も2、3問ありました。(e.g., ある人は配列の要素数を"確定"したい。選択肢の中でどの関数を使えば良いか?)

公式の出題範囲はJSの入門レベルの内容と書いていましたが、筆者は1.5年のWebサーバー開発経験があって、一般的なJS入門書(改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで)も2回読破しましたが、それでも落ちるというのは設問が偏っていると考えています。

最後に

これからの受験予定者のために、受験した方はコメント欄に試験の感想とか実際の出題範囲を載せて頂ければ幸いです。

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