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

【React】AWSにデプロイする

はじめに

React を習得するまでの軌跡をメモっていく備忘録的な記事です。

AWS の S3 にデプロイする

S3 の静的サイトホスティング機能を使ってデプロイします。

準備

サンプルコードを clone してきます。
中身は redux-toolkit の公式サイトで紹介されているサンプルのTODOアプリです。

$ git clone https://github.com/reduxjs/rtk-convert-todos-example.git

アプリのディレクトリに移動し yarn build を実行します。

$ cd rtk-convert-todos-example
rtk-convert-todos-example $ yarn build

アプリの中に新しく build というディレクトリができたと思います。
確認してみましょう。

rtk-convert-todos-example $ ls
README.md   jsconfig.json   package.json    src
build       node_modules    public      yarn.lock

build ディレクトリができていますね。
build ディレクトリの中身も見てみましょう。

rtk-convert-todos-example $ ls build
asset-manifest.json
favicon.ico
index.html
logo192.png
logo512.png
manifest.json
precache-manifest.4cc66b9b5865646273439dd929f332c4.js
robots.txt
service-worker.js
static

色々なファイルができていますね。

これらをS3にアップロードします。

デプロイしていく

まずS3バケットを作りましょう。
「バケットを作成」と言うボタンをおしてください。

スクリーンショット 2020-11-29 23.31.40.png

バケット名を入力してください。
他の設定などは全てデフォルトで大丈夫です。

スクリーンショット 2020-11-29 23.26.22.png

バケットの作成ができたらS3のバケット一覧画面から先ほど作ったバケットを選択するとバケットの編集画面に遷移します。

スクリーンショット 2020-11-29 23.36.19.png

この中でプロバティというタブをクリックすると静的ウェブサイトホスティングという項目が出てきます。
この項目の設定を変更したいので編集ボタンをクリックします。

スクリーンショット 2020-11-29 23.39.46.png

  • 静的webサイトホスティングを有効に
  • インデックスドキュメントは index.htmlと入力してください。
  • エラードキュメントは error.htmlと入力してください。
  • 他はデフォルトで大丈夫です。

設定ができたらバケットの編集画面からオブジェクトタブをクリックしてファイルをアップロードします。
build ディレクトリの中身をそのままドラッグアンドドロップで大丈夫です。

このままだとアクセスできないので設定をを変更します。
バケットの編集画面からアクセス許可のタブをクリックしてください。

バケットポリシーを設定します。。
arn:aws:s3:::awsexamplebucket1の部分を自分の ARN に変えてそのままコピペして貼り付けるだけで大丈夫です。

{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"PublicRead",
      "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject","s3:GetObjectVersion"],
      "Resource":["arn:aws:s3:::awsexamplebucket1/*"]
    }
  ]
}

ブロックパブリックアクセスは上の二つだけチェックして他はチェックを外してください。
スクリーンショット 2020-11-29 23.46.19.png

プロパティの静的ウェブサイトホスティングに記載のあるURLにアクセスし、以下のようなTODOアプリが表示されたら成功です。

スクリーンショット 2020-11-29 23.53.00.png

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

【React】 AWSにデプロイする

はじめに

React を習得するまでの軌跡をメモっていく備忘録的な記事です。

AWS の S3 にデプロイする

S3 の静的サイトホスティング機能を使ってデプロイします。

準備

サンプルコードを clone してきます。
中身は redux-toolkit の公式サイトで紹介されているサンプルのTODOアプリです。

$ git clone https://github.com/reduxjs/rtk-convert-todos-example.git

アプリのディレクトリに移動し yarn build を実行します。

$ cd rtk-convert-todos-example
rtk-convert-todos-example $ yarn build

アプリの中に新しく build というディレクトリができたと思います。
確認してみましょう。

rtk-convert-todos-example $ ls
README.md   jsconfig.json   package.json    src
build       node_modules    public      yarn.lock

build ディレクトリができていますね。
build ディレクトリの中身も見てみましょう。

rtk-convert-todos-example $ ls build
asset-manifest.json
favicon.ico
index.html
logo192.png
logo512.png
manifest.json
precache-manifest.4cc66b9b5865646273439dd929f332c4.js
robots.txt
service-worker.js
static

色々なファイルができていますね。

これらをS3にアップロードします。

デプロイしていく

まずS3バケットを作りましょう。
「バケットを作成」と言うボタンをクリックしてください。

スクリーンショット 2020-11-29 23.31.40.png

バケット名を入力してください。
他の設定などは全てデフォルトで大丈夫です。

スクリーンショット 2020-11-29 23.26.22.png

バケットの作成ができたらS3のバケット一覧画面から先ほど作ったバケットを選択するとバケットの編集画面に遷移します。

スクリーンショット 2020-11-29 23.36.19.png

この中でプロバティというタブをクリックすると静的ウェブサイトホスティングという項目が出てきます。
この項目の設定を変更したいので編集ボタンをクリックします。

スクリーンショット 2020-11-29 23.39.46.png

  • 静的webサイトホスティングを有効に
  • インデックスドキュメントは index.htmlと入力してください。
  • エラードキュメントは error.htmlと入力してください。
  • 他はデフォルトで大丈夫です。

設定ができたらバケットの編集画面からオブジェクトタブをクリックしてファイルをアップロードします。
build ディレクトリの中身をそのままドラッグアンドドロップで大丈夫です。

このままだとアクセスできないので設定をを変更します。
バケットの編集画面からアクセス許可のタブをクリックしてください。

バケットポリシーを設定します。。
arn:aws:s3:::awsexamplebucket1の部分を自分の ARN に変えてそのままコピペして貼り付けるだけで大丈夫です。

{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"PublicRead",
      "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject","s3:GetObjectVersion"],
      "Resource":["arn:aws:s3:::awsexamplebucket1/*"]
    }
  ]
}

ブロックパブリックアクセスは上の二つだけチェックして他はチェックを外してください。
スクリーンショット 2020-11-29 23.46.19.png

プロパティの静的ウェブサイトホスティングの項目に記載のあるURLにアクセスし、以下のようなTODOアプリが表示されたら成功です。

スクリーンショット 2020-11-29 23.53.00.png

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

【Vue.js】cowsayをブラウザ上に表示させてみた

はじめに

スクリーンショット 2020-11-29 23.07.31.png
cowsayがかわいいのでVue.jsでブラウザ上に表示させたくなりました。

インストール

cowsay-browserを使用します。

ターミナル
npm install cowsay-browser

インポート

cowsay-browserをインポートします。

とりあえずコンソールに表示させてみます。

App.vue
<script>
import cowsay from 'cowsay-browser'

export default {
  created() {
    console.log(cowsay.say({text : "Hello!"}));
  }
}
</script>

スクリーンショット 2020-11-29 23.19.33.png

来た!!

あとはブラウザに表示させるだけです。

ブラウザへ

App.vue
<template>
  <div>
    <pre>{{ cowsay }}</pre>
  </div>
</template>

<script>
import cowsay from 'cowsay-browser'
export default {
  data() {
    return {
      cowsay: '' 
    }
  },
  created() {
    this.cowsay = cowsay.say({text : "Hello!"});
  }
}
</script>

スクリーンショット 2020-11-29 23.23.47.png


な、なんかズレて...る.......

調整する

せっかくなので少しソースコードをいじります。
node_modules>cowsay-browser>lib>cow-definitions.jsをいじればいけそうです。

cow-definitions.js
//-------省略


  "default": "$the_cow = <<\"EOC\";\r\n        $thoughts   ^__^\r\n         $thoughts  ( $eyes )\\\\_____\r\n             (__)\\\\              )\\\\/\\\\\r\n             $tongue       ||-------w |\r\n                      ||             ||\r\nEOC\r\n",


//-------省略

スクリーンショット 2020-11-29 23.36.43.png
・・・まあいいでしょう!(投げやり)

おしまい

スクリーンショット 2020-11-29 23.47.52.png

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

OSSライセンス遵守をよにり具体的にこ

はじめに
これまで依存関係にあるパッケージ等のライセンスを取得してくれるGitHub製のlicensedやこの他にも様々なライセンスチェッカーが開発されてきました。
特にlicensedはカバーされる範囲も広く個人的にかなり正確に取得してくれると感じていますが利用手順が多いということも挙げられます。
今回、3つの手順だけでこのようなライセンスチェック機能に加えライセンス遵守のためにするべきことを一つのテキストファイルにまとめて教えてくれるnpmパッケージを開発しました。(npmのみ対応)

doppelganger-license
https://medium.com/@hotmailyougo/folding-bikes-market-business-strategies-technological-innovation-trends-top-players-by-2030-c26cc68f9a69
今回開発したパッケージをdoppelganger-licenseと名付けました。
詳細はGitHub Repoに記載しています。
doppelganger-licenseは先述した通り、依存関係にあるライセンスを取得しライセンス遵守のためにすべきことを具体的に教えてくれます。
これらの機能は全てプロジェクトファイル内のpackage-lock.jsonに基づいています。

手順としては

1. doppelganger-licenseのインストール

$ npm install doppelganger-license
2. dependencies.txtとmemo.txtをお好きな場所に作成
  dependencies.txtにライセンス条項が自動出力され、memo.txtにやることリストが出力されます。

3. doppelganger-licenseを実行

$ doppelganger-license
package-lock.json, dependencies.txt, memo.txtのパスが質問されるのでそれぞれ答えてください

以上の3手順で利用できます。

デモ動画はレポジトリでご確認いただけます。

仕組み
仕組みはそこまで複雑ではなく

package-lock.jsonのdependenciesにあるパッケージの名前・バージョンをfsを用いて抽出
                    ↓
    node-fetch経由でregistry.npmjs.orgにてパッケージのライセンスを取得
                    ↓
      node-fetch経由でjsdelivrにてパッケージのライセンス条項を取得
                    ↓
      取得したライセンス条項をfsを用いてdependencies.txtに出力
                    ↓
   コピーレフトライセンスを発見した場合はそのライセンスの条件をmemo.txtに出力

という流れです。

今後の発展予定
現時点でライセンス間の矛盾などは考慮できておらず、一部のコピーレフトライセンスは検出できない場合がありまだまだ改善の余地はあるという状態です。
バグの修正や改善点の報告・要望などは早くお応えできるかと思いますので、よろしければGitHub Repoにてご報告いただけますと幸いです。よろしくお願いいたします。

K-Rintaro/doppelganger-license - GitHub

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

BIRCHをJavaScriptで実装した

はじめに

色々な機械学習処理をブラウザ上で試せるサイトを作った」中で実装したモデルの解説の八回目です。

今回はBIRCHの実装について解説します。

デモはこちらから。(TaskをClusteringにして、ModelのBIRCHを選択)
実際のコードはbirch.jsにあります。

なお、可視化部分については一切触れません。

概説

アルゴリズムについては、こちらが詳しいです。

ただし今回の実装は、Phase1のみとなります。

Phase3も必須とされていますが、現在のところ木の根直下のノードを分類されたクラスタとして考えているため、実装していません。
(なので、厳密には元論文のままのBIRCHではないことに注意してください)

実装する場合は、まずクラスタ間距離を計算する部分を作成し、そしてその距離を用いてクラスタリングを行う処理を作成します。

コード

CF Tree

ノーテーションはWikipediaを中心に、rの計算はブログの方のクラスタ直径を採用しています。

pushが呼び出されてデータが追加されるたびに、分割の判定を行います。
分割は_separateで行いますが、根や葉の場合は個別の処理が必要になるので、その部分が肥大化しています。
また、cfおよびrは何度も呼び出され、かつ計算が重いので、遅延評価のような感じに実装しています。

class CFTree {
    constructor(b = 10, t = 0.2, l = Infinity) {
        this._b = b
        this._l = l
        this._t = t
        this._datas = [];
        this._children = [];
        this._parent = null
    }

    get size() {
        return this._datas.length
    }

    get length() {
        return this._children.length
    }

    get depth() {
        if (this.isLeaf()) {
            return 1
        }
        return this._children.reduce((s, v) => Math.max(s, v.depth + 1), -Infinity)
    }

    get cf() {
        if (this._cf) {
            return this._cf
        }
        if (this.isLeaf()) {
            const n = this._datas.length;
            if (n === 0) {
                this._cf = {
                    n: 0,
                    ls: null,
                    ss: 0
                }
            } else {
                const ls = Array(this._datas[0].length).fill(0)
                let ss = 0
                for (let i = 0; i < n; i++) {
                    for (let j = 0; j < this._datas[i].length; j++) {
                        ls[j] += this._datas[i][j]
                        ss += this._datas[i][j] ** 2
                    }
                };
                this._cf = {
                    n: n,
                    ls: ls,
                    ss: ss
                }
            }
        } else {
            this._cf = {
                n: this._children[0].cf.n,
                ls: this._children[0].cf.ls.concat(),
                ss: this._children[0].cf.ss
            }
            for (let i = 1; i < this.length; i++) {
                const cf = this._children[i].cf
                this._cf.n += cf.n
                this._cf.ss += cf.ss
                for (let j = 0; j < cf.ls.length; j++) {
                    this._cf.ls[j] += cf.ls[j]
                }
            }
        }
        return this._cf
    }

    get r() {
        if (this._r) {
            return this._r
        }
        if (this.isLeaf()) {
            const n = this._datas.length
            if (n <= 1) {
                this._r = Infinity
            } else {
                let r = 0;
                for (let i = 0; i < n; i++) {
                    const di = this._datas[i]
                    for (let j = 0; j < i; j++) {
                        r += 2 * this._datas[j].reduce((s, v, k) => s + (v - di[k]) ** 2, 0)
                    }
                }
                this._r = Math.sqrt(r / (n * (n - 1)))
            }
        } else {
            this._r = this._children.reduce((m, c) => Math.max(m, c.r), -Infinity)
        }
        return this._r
    }

    get c() {
        return this.cf.ls.map(v => v / this.cf.n)
    }

    at(index) {
        return this._children[index]
    }

    isRoot() {
        return this._parent === null
    }

    isLeaf() {
        return this._children.length === 0;
    }

    push(data) {
        this._cf = null
        this._r = null
        if (!this.isLeaf()) {
            let min_d = Infinity
            let min_i = -1
            for (let i = 0; i < this.length; i++) {
                const c = this._children[i].c
                const d = data.reduce((s, v, k) => s + (v - c[k]) ** 2, 0)
                if (d < min_d) {
                    min_d = d
                    min_i = i
                }
            }
            this._children[min_i].push(data)
            if (this._children.length >= this._b) {
                this._separate();
            }
            return
        }
        this._datas.push(data);
        if (this._datas.length <= 2) return
        if (this._datas.length >= this._l || this.r > this._t) {
            this._separate();
        }
    }

    _separate() {
        const d = this.isLeaf() ? this._datas : this._children.map(c => c.c)
        const model = new KMeansModel();
        model.method = new KMeanspp();
        model.add(d);
        model.add(d);
        while (model.fit(d) > 0);
        const p = model.predict(d);
        if (this.isLeaf()) {
            if (this.isRoot()) {
                const c1 = new CFTree(this._b, this._t, this._l);
                c1._datas = d.filter((v, i) => p[i] === 0);
                c1._parent = this
                const c2 = new CFTree(this._b, this._t, this._l);
                c2._datas = d.filter((v, i) => p[i] === 1);
                c2._parent = this
                this._children = [c1, c2]
                this._datas = null
            } else {
                const new_c = new CFTree(this._b, this._t, this._l);
                new_c._datas = d.filter((v, i) => p[i] === 1);
                new_c._parent = this._parent
                this._parent._children.push(new_c)
                this._datas = d.filter((v, i) => p[i] === 0)
            }
        } else {
            if (this.isRoot()) {
                const c1 = new CFTree(this._b, this._t, this._l)
                c1._children = this._children.filter((c, i) => p[i] === 0)
                c1._children.forEach(c => c._parent = c1)
                c1._parent = this
                const c2 = new CFTree(this._b, this._t, this._l)
                c2._children = this._children.filter((c, i) => p[i] === 1)
                c2._children.forEach(c => c._parent = c2)
                c2._parent = this
                this._children = [c1, c2]
            } else {
                const new_c = new CFTree(this._b, this._t, this._l)
                new_c._children = this._children.filter((c, i) => p[i] === 1);
                new_c._children.forEach(c => c._parent = new_c)
                new_c._parent = this._parent
                this._parent._children.push(new_c)
                this._children = this._children.filter((c, i) => p[i] === 0)
            }
        }
    }
}

BIRCH

CF Treeの根直下のノードそれぞれをクラスタと見ることにしているため、かなりシンプルになっています。
これを、クラスタ間距離も考慮した実装とする場合は、もう少し複雑になります。

class BIRCH {
    // https://en.wikipedia.org/wiki/BIRCH
    // http://somathor.hatenablog.com/entry/2013/04/23/205320
    constructor(k, b, t, l) {
        this._k = k
        this._tree = new CFTree(b, t, l)
    }

    fit(datas) {
        for (let i = 0; i < datas.length; i++) {
            this._tree.push(datas[i])
        }
    }

    predict(datas) {
        if (this._tree.isLeaf()) {
            return Array(datas.length).fill(0);
        }
        const centers = this._tree._children.map(c => c.c);
        return datas.map(data => {
            let min_d = Infinity
            let min_i = -1
            for (let i = 0; i < centers.length; i++) {
                const d = data.reduce((s, v, k) => s + (v - centers[i][k]) ** 2, 0)
                if (d < min_d) {
                    min_d = d
                    min_i = i
                }
            }
            return min_i;
        })
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

svelte.jsでゲームをつくってみた

この記事の概要

svelte.jsで間違い探しゲームをつくってみました。いろいろとハマったポイントもあったので忘れないようにメモしていきます。

作ったモノの概要

ピクトグラムを使った間違い探しゲームです。

svelte_1.gif

チュートリアルも実装されています。

svelte_2.gif

公開先

使ったフレームワーク

  • svelte.js (JavaScript全体のフレームワーク)
  • driver.js (チュートリアル用のライブラリ)
  • bulma (CSSのフレームワーク)

svelte.jsを使って困ったこと

npm installで他のパッケージが上手くインストールできない

webpackあたりでエラーが出てインストールできなかったモノがあったので、一旦はindex.htmlCDNで利用するようにしました。ここらへんはもう少し調査が必要そうです。今回は作ることを優先したので一旦CDNで利用する形を取りました。

配列を追加しても再描画されない

公式のドキュメントにも書いていますが配列を追加しただけだと画面上のオブジェクトが更新されません。
ログで出力するとちゃんと変数に入っているので、結構ココでハマるポイントかもしれません。
それじゃどうすればいいのかというと、変数として再代入することで解決します。svelte.jsの仕様上、変数に再代入されたときだけちゃんと再描画として認識するようです。vue.jsで慣れていると「ナンデダヨ!!」っていう部分です。

ちなみに今回のソースで該当の部分は以下のとおり

<script>
    export const changeImage = () => {
        setAnimation();
        imgArray = [];
        const appearRnd = Math.ceil(Math.random() * appearImg) - 1;
        const imageRnd = Math.ceil(Math.random() * imageCount);
        for (let step = 0; step < appearImg; step++) {
            if (appearRnd == step) {
                // imgArray.push(`pict_img/${imageRnd}-A.png`);
                // ※pushの場合は変数に再代入する必要がある
                // imgArray = imgArray;
                imgArray = [...imgArray, `pict_img/${imageRnd}-A.png`];
            } else {
                // imgArray.push(`pict_img/${imageRnd}-B.png`);
                imgArray = [...imgArray, `pict_img/${imageRnd}-B.png`];
            }
        }
    };
</script>

{#if isVisible}
    <div transition:fly={{ y: 100 }} class="container has-text-centered">
        {#each imgArray as img}
            <BtnPict imagePath={img} clickFunc={clickAction} />
        {/each}
    </div>
{/if}

使う機能をimportする必要がある

vue.jsだと初期で読み込むcreatedmountedなどはimportせずに利用できます。svelte.jsの場合は以下のように、その都度読み込む必要があるため面倒です。もしかすると上手くやる方法があるかもしれません。

<script>
    // onMountを使うなら毎回宣言する必要がある
    import { onMount } from "svelte";
</script>

動的にon:clickに対して関数を設定するときに注意が必要

on:clickに関数を設定するときにそのまま設定すると死にます。引数がない場合は()なしで設定すればよいのですが、引数付きの関数の場合は気をつける必要があります。

以下はダメパターン。

<script>
    export let clickFunc = (msg) => console.log(msg);
</script>

<p on:click={clickFunc ('コンソールログがタイヘンなことに・・・')}>TEST</p>

以下はイイパターン。

<script>
    export let clickFunc = (msg) => console.log(msg);
</script>

<p on:click={() => {clickFunc ('ヤッホー')} }>TEST</p>

svelte.jsを使ってよかったこと

変数の扱いがわかりやすい

vue.jsに比べてそのままJavaScriptの変数を扱うような感じです。具体的にいうとvue.jsの場合はテンプレートに渡す変数はpropsとして渡す必要があります。そしてpropsから渡ってきた変数はそのまま再代入するとwarningで怒られます。dataで変数を宣言しなおして再代入することが正しいわけです。

vue.jsですが以下はwarningで怒られます。

export default {
  props: {
    propsValue: Number
  },
  methods: {
    editValue() {
      this.propsValue = 20
    }
}

vue.jsでは以下が正しい姿

export default {
  props: {
    propsValue: Number
  },
  data() {
    // プロパティの値を設定
    dataValue: this.propsValue
  },
  methods: {
    editValue() {
      // データに定義した値を変更
      this.dataValue = 20
    }
}

ここまでvue.jsのことを書いてきましたが、propsdata()でそれぞれの変数の扱いがありますが、svelte.jsだとそういった扱いの違いがなく直感的に扱えるのは良い点だと思いました。

svelte.jsで違うコンポーネントに渡したい場合は・・・

■ App.svelte

<script>
    import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>

exportをつけるだけで渡せます。

■ Nested.svelte

<script>
    export let answer;
</script>

<p>The answer is {answer}</p>

読み取り専用のストアの変数を設定できる

上記のようにコンポネントで変数を渡し合うことでどこで何の変数が使われているか把握しづらくなります。1対1で渡すだけならいいのですが、コンポネントに渡してその先のコンポネントに渡してのバケツリレーになったりすると悲惨です。

そのためにアプリケーションで扱うデータセットをstoreと呼ばれる領域で一元管理することで、各コンポーネントはstoreにアクセスすれば常に共通の値を参照する仕組みがあります。これはvue.jsでも同様でいろいろなコンポネントから値を参照する場合に有効だったりします。

ここでもvue.jsの話になりますが、vue.jsの場合はstoregettersetterを記述して、storeの変数を参照したいときはgetterを使い、storeの変数を更新したいときはsetterを使うわけです。

svelte.jsでもstoreを使えるわけですがよりわかりやすく使用できます。

■store.js

import { writable } from 'svelte/store';
export const ST_score = writable(0);

■App.js

<script>
    import { ST_score, ST_gamestart } from "./stores.js";
    import { get } from "svelte/store";
    //更新
    ST_score.set(100);

    //他のコンポネントから更新されたか検知したい場合はsubscribeを利用
    let score = 0;
    ST_score.subscribe((value) => {
        score = value;
    });

    //ちなみにsvelte/storeのgetでも取得はできる
    //(ただし他での更新は検知されない)
    //初期値を代入するときに利用できそう
    console.log(get(ST_score));

</script>

ただし扱いやすくなる反面、使われたくないタイミングで使われるケースもあるわけです。例えば、変なところで変数を更新されたくないため、読み取り専用にしたいな~というケースもあるわけです。そこでsvelte.jsではJavaとかでいうところのカプセル化のような隠蔽化するような仕組みもあります。

今回の作ってみたモノではハイスコアを読み取り専用にしてスコアの状態からハイスコアを更新するようにしています。ちなみにハイスコアはローカルストレージに保存するようにしています。

import { readable, writable } from 'svelte/store';
import { get } from 'svelte/store';

const lsKey_hiScore = "hiscore"
let lsHiScore = localStorage.getItem(lsKey_hiScore);
lsHiScore = lsHiScore ? lsHiScore : 0;

export const ST_score = writable(0);
export const ST_hiScore = readable(null, function start(set) {
    //readableで初期値をセットできる
    set(lsHiScore);

    //この中だけで値の更新ができる
    //スコアの変更を検知して条件によって更新する
    ST_score.subscribe(value => {
        console.log('hi-score: ' + get(ST_score));
        if (lsHiScore < value) {
            set(value);
            localStorage.setItem(lsKey_hiScore, value);
        }
    });
});

最初から使えるトランジションが便利(効果や演出がラク)

フェードインやスクロールインのアニメーションなどカンタンに使えます。
時間の設定なども細かくできるので他のCSSのライブラリとか使わずアニメーションを設定出来るのは便利かなと。
ただし、使うトランジションごとにインポートは必要で、コンポネント各所で利用すると思ったとおりの演出にならないこともあるので、イイカンジにお付き合いする必要があります。
実際にトランジションを使う場合は、ifなどで切り替わることでトランジションを発生させることができるようです。もしも初回の表示でトランジションを使いたい場合は、onMountなどで切り替えるとよさそうです。

<script>
    import { onMount } from "svelte";
    import { fly } from "svelte/transition";
    let isVisible = false;

    onMount(() => {
        let isVisible = true;
    });
</script>

{#if isVisible}
    <p transition:fly={{ y: 100 }}>
       下から上にせり上がる感じのやーつ 
    </p>
{/if}

総評

最初こそはいろいろとvue.jsとの違いで文句があったりしましたが、あとになればなるほどサクサク書けるようになりました。
storeによるreadableですが設定することで他のstore変数をトリガーにする形にしているので面倒くさいことになったかなと。
あとは1つのコンポネントが大きくなってきたら細かく分けたり、ここらへんはvue.jsと同様のセンスが必要だったりします。
あとはチュートリアルのライブラリがかゆいところに手が届かなくて試行錯誤したのが苦労したところかなと思います。

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

Javascript、JQueryコード比較。

普段はJQueryを使っているのですが、プロジェクトによっては素のJavascriptで書いてくれと指定があったりします。僕も過去このような経験があってJQueryだったらもっと簡単に・・・なんて思いながらcodeingしたりしたことがあります?

なので今回はある機能を題材にコードの比較をしていきます。是非参考にしていって下さい。

ちなみにJQueryは、モダンなフレームワークにどんどんシェアを奪われているのでそろそろreactやvueあたりに手をつけたいと思っています。

Javascriptとは

webアプリケーションに対して動的コンテンツを盛り込むためのプログラミング言語です。
webサイトを例とると、スライダーやスクロール時のフェードインなど動きのあるコンテンツを作成する事ができます。

JQueryとは

javascriptのライブラリーです。
簡単に言ってしまうと、JQuery側が用意してる独自のコードを使うことでjavascriptを容易に使用する事ができます。

よく勘違いされてるかたがいるのですが、フレームワークではありません。フレームワークはアプリの半完成品ですのでこの時点でJQueryのコンセプトとはズレてます。

コード比較

さて今回の本題です。実際にjavascriptとJQueryでどれだけコード量が変わるでしょうか?今回はモーダルウインドウという機能を作ってみましたので比較してみます。

JQueryのモーダルはこちらを参考にさせて頂きました。

e619d9964d6c3628d3ce71e725bfd797.gif

動き的にはこんな感じになります。ページ遷移せずポップアップのような別の画面を表示することが出来ます。
ではコード記載していきます。

html
<body>
    <div class="content">
      <a class="js-modal-open" href="" data-target="modal01">クリックでモーダル1を表示</a><br>
      <a class="js-modal-open" href="" data-target="modal02">クリックでモーダル2を表示</a>
  </div>
  <div id="modal01" class="modal js-modal">
      <div class="modal__bg js-modal-close"></div>
      <div class="modal__content">
          <p>1つ目モーダルウィンドウです。ここにモーダルウィンドウで表示したいコンテンツを入れます。モーダルウィンドウを閉じる場合は下の「閉じる」をクリックするか、背景の黒い部分をクリックしても閉じることができます。</p>
          <a class="js-modal-close" href="#">閉じる</a>
      </div><!--modal__inner-->
  </div><!--modal-->
  <div id="modal02" class="modal js-modal">
      <div class="modal__bg js-modal-close"></div>
      <div class="modal__content">
          <p>2つ目モーダルウィンドウです。ここにモーダルウィンドウで表示したいコンテンツを入れます。モーダルウィンドウを閉じる場合は下の「閉じる」をクリックするか、背景の黒い部分をクリックしても閉じることができます。</p>
          <a class="js-modal-close" href="#">閉じる</a>
      </div><!--modal__inner-->
  </div><!--modal-->
</body>
</html>
css
/* modal window */
.content{
  margin: 0 auto;
  padding: 40px;
}
.modal{
  display: none;
  height: 100vh;
  position: fixed;
  top: 0;
  width: 100%;
}
.modal.open{
  display: block;
}
.modal__bg{
  background: rgba(0,0,0,0.8);
  height: 100vh;
  position: absolute;
  width: 100%;
}
.modal__content{
  background: #fff;
  left: 50%;
  padding: 40px;
  position: absolute;
  top: 50%;
  transform: translate(-50%,-50%);
  width: 60%;
}
JQuery
$(function(){
  $('.js-modal-open').each(function(){
    $(this).click(function(){
      var target = $(this).data('target');
        $("#" + target).addClass("open");
        return false;
    });
  });
  $('.js-modal-close').click(function(){
    $('.js-modal').removeClass("open");
      return false;
  });
});

JQueryだと10行ちょっとで実装出来ました。
Javascriptだとどうでしょうか。

Javascriptの場合。

javascript
window.onload = function () {
  var $openModal = document.getElementsByClassName("js-modal-open");
  let $closeModal = document.getElementsByClassName("js-modal-close");
  for( var $i = 0; $i < $openModal.length; $i++ ) {
    $openModal[$i].onclick = function (event) {
      event.preventDefault();
      var $target = this.dataset.target;
      var $modal = document.getElementById($target);
      $modal.classList.add('open');
    }
  }
  for( var $i = 0; $i < $closeModal.length; $i++ ) {
    $closeModal[$i].onclick = function (event) {
      event.preventDefault();
      this.closest(".js-modal").classList.remove('open');
    }
  }
}

機能や動きは同じです。
どうでしょう?あなたならどっちを使って開発を進めたいですか?大半の人はJQueryを選ぶでしょう。スピードや将来性を見込んでjavascriptを選ぶ人もいるかも知れませんが、コード量がやはり決め手になるかと思います。

個人的な意見ですがJQueryの魅力はセレクタの指定方法でしょうか。
JavascriptだとgetElementByXXXと書くところを、JQueryでは$("xxx")で済ませることが出来ます。今回の場合でいうとeach文を使えるのも魅力でしょう。

最後に

今回は比較だけで内容が非常に薄くなってしまいましたが、今後は何か知らの機能の実装など紹介できたらなと思います。

補足

@il9437 さんよりご指摘を頂き、以下でも実装出来ることが分かりました。本当にありがとうございました。

javascriptコードご指摘修正後(querySelector,forEach使用)
window.onload = function () {
  var $openModal = document.querySelectorAll(".js-modal-open");
  var $closeModal = document.querySelectorAll(".js-modal-close");
  $openModal.forEach(function(openItem){
    openItem.onclick = function (event) {
      event.preventDefault();
      var $target = this.dataset.target;
      var $modal = document.getElementById($target);
      $modal.classList.add('open');
    }
  });
  $closeModal.forEach(function(closeItem){
    closeItem.onclick = function (event) {
      event.preventDefault();
      this.closest(".js-modal").classList.remove('open');
    }
  });
}

確かにこちらの方が綺麗ですね。結構昔にJSをちょっと学んでJQueryに切り替えたのでまだまだ知らないことたくさんありそうですね?ご指摘頂いた方には本当に感謝です。

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

OSSライセンス遵守をより具体的に

はじめに

これまで依存関係にあるパッケージ等のライセンスを取得してくれるGitHub製のlicensedやこの他にも様々なライセンスチェッカーが開発されてきました。
特にlicensedはカバーされる範囲も広く個人的にかなり正確に取得してくれると感じていますが利用手順が多いということも挙げられます。
今回、3つの手順だけでこのようなライセンスチェック機能に加えライセンス遵守のためにするべきことを一つのテキストファイルにまとめて教えてくれるnpmパッケージを開発しました。(npmのみ対応)

doppelganger-license

今回開発したパッケージをdoppelganger-licenseと名付けました。
詳細はGitHub Repoに記載しています。
doppelganger-licenseは先述した通り、依存関係にあるライセンスを取得しライセンス遵守のためにすべきことを具体的に教えてくれます。
これらの機能は全てプロジェクトファイル内のpackage-lock.jsonに基づいています。

手順としては

1. doppelganger-licenseのインストール

$ npm install doppelganger-license

2. dependencies.txtとmemo.txtをお好きな場所に作成
  dependencies.txtにライセンス条項が自動出力され、memo.txtにやることリストが出力されます。

3. doppelganger-licenseを実行

$ doppelganger-license

package-lock.json, dependencies.txt, memo.txtのパスが質問されるのでそれぞれ答えてください

以上の3手順で利用できます。

デモ動画はレポジトリでご確認いただけます。

仕組み

仕組みはそこまで複雑ではなく

package-lock.jsonのdependenciesにあるパッケージの名前・バージョンをfsを用いて抽出
                    ↓
    node-fetch経由でregistry.npmjs.orgにてパッケージのライセンスを取得
                    ↓
      node-fetch経由でjsdelivrにてパッケージのライセンス条項を取得
                    ↓
      取得したライセンス条項をfsを用いてdependencies.txtに出力
                    ↓
   コピーレフトライセンスを発見した場合はそのライセンスの条件をmemo.txtに出力

という流れです。

今後の発展予定

現時点でライセンス間の矛盾などは考慮できておらず、一部のコピーレフトライセンスは検出できない場合がありまだまだ改善の余地はあるという状態です。
バグの修正や改善点の報告・要望などは早くお応えできるかと思いますので、よろしければGitHub Repoにてご報告いただけますと幸いです。よろしくお願いいたします。

K-Rintaro/doppelganger-license - GitHub

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

ESLint&Prettierでまさかコードが壊れるなんて

この記事はGoodpatch Advent Calendar 2020の2日目となります。
最寄り駅のイチョウ並木にイルミネーションが灯り始めました。

JSを整形したいだけだった

フロントエンド開発では定番のeslintprettier
今回やりたいことはJavaScriptコードをeslintでコードチェックし、prettierでコード整形したいだけ。
それだけのはずでした。

目次

使用ファイル

今回説明するコードサンプルは以下GitHubにまとめてあります。
こちらも合わせてご参照ください。
https://github.com/ikezaworld/eslinttest

ディレクトリ構成

/
├─ src
│  ├─ index.js
│  └─ index_org.js
├─ .eslintrc.js
├─ .prettierrc.js
└─ package.json

何の変哲もないコードたち

対象コードは同じ内容の三項演算子が3つあるだけ。
見分けやすいよう変数名の連番(test1 test2 test3)のみ変えています。
名前以外は全くいっしょのコードたちです。

src/index.js
const test1 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


const test3 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


整形を実行する

さっそく eslint --fix ./src/index.js を実行。
すると以下の結果になりました。

▼ 実行結果

src/index.js
const test1 = isFoo
  `データfooを選択中です(${  1 } 個)` : '選択できるデータは存在しません';

const test2 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";

const test3 = isFoo
  `データfooを選択中です(${  1 } 個)` : '選択できるデータは存在しません';

結果がおかしい!?

消えた三項演算子?

test1 test3isFoo 後ろの三項演算子の ? が消えています。
これでは構文エラーとなりJavaScript実行ができない。
何だろうこれは。

無事だった2番目

全て壊れるのであれば、元々のコード側に問題があった可能性もあります。
しかし2番目の test2 は正常に整形されています。
ただ、テンプレートリテラルは適用されていません。
どういうことだろう。

色んなパターンを試す

三項演算子?の直後に半角スペース

test1 の三項演算子 ? 直後に半角スペースを入れて実行してみる。

▼ 実行前

src/index.js
const test1 = isFoo ? 
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


const test3 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


▼ 実行結果

src/index.js
const test1 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";

const test3 = isFoo
  `データfooを選択中です(${  1 } 個)` : '選択できるデータは存在しません';

カテゴリ 結果
test1
test2
test3 ×

※△は構文はOKだがテンプレートリテラルがされていないもの
実行結果はtest1 がOKになった。他はそのまま変化なし。

三項演算子?の直後の改行を消し1行にする

test1 の三項演算子 ? 直後の改行を消し1行にしてみる。

▼ 実行前

src/index.js
const test1 = isFoo ?  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


const test3 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


▼ 実行結果

src/index.js
const test1 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";

const test2 = isFoo
  `データfooを選択中です(${  1 } 個)` : '選択できるデータは存在しません';

const test3 = isFoo
  `データfooを選択中です(${  1 } 個)` : '選択できるデータは存在しません';

カテゴリ 結果
test1
test2 ×
test3 ×

test1 がOKになった。
しかし成功していたはずの test2 が壊れてしまった。

コード間の改行を1行に揃える

test1 test2 test3 のコード間の改行を1行に揃えてみる。

▼ 実行前

src/index.js
const test1 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

const test3 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

▼ 実行結果

src/index.js
const test1 = isFoo
  `データfooを選択中です(${  1 } 個)` : '選択できるデータは存在しません';

const test2 = isFoo
  `データfooを選択中です(${  1 } 個)` : '選択できるデータは存在しません';

const test3 = isFoo
  `データfooを選択中です(${  1 } 個)` : '選択できるデータは存在しません';

カテゴリ 結果
test1 ×
test2 ×
test3 ×

実行結果は、なんと全部失敗になってしまった。改行消しただけなのに。
これはひどい。

失敗から見えてくるもの

色んなパターンを試してみた。
結局どのパターンでも3つ全部成功はできなかった。
しかしこれまでのパターンから、

  • 改行
  • スペース

の有無にどうやら意味があるらしいことが見えてきた。

設定ファイル内容

package.json

package.json は以下の通り。

package.json
{
  "name": "eslinttest",
  "version": "1.0.0",
  "description": "test of eslint & prettier",
  "scripts": {
    "eslint:fix": "eslint --fix ./src/index.js",
    "prettier": "prettier --write ./src/index.js"
  },
  "devDependencies": {
    "eslint": "^7.14.0",
    "eslint-config-prettier": "^6.15.0",
    "eslint-plugin-prettier": "^3.1.4",
    "prettier": "^2.2.0"
  }
}

4つのモジュールをインストールしています。
versionは執筆時点で最新のものを使っています。

  • eslint (eslint本体です)
  • eslint-config-prettier (ESLint内でPrettierと競合するルールをオフにしてくれる)
  • eslint-plugin-prettier (PrettierをESLintルールとして実行できるようにする)
  • prettier (prettier本体です)

後は整形呼び出ししやすいようnpm scriptを書いているだけです。

ESLint設定

ESLint設定を行っている.eslintrc.js 内容は以下の通り。

.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es6: true,
  },
  extends: ["prettier"],
  plugins: ["prettier"],
  rules: {
    "prettier/prettier": "warn",
    "prefer-template": "warn",
  },
};

ESLintの設定はprettierが実行できるよう最低限の設定をしています。
文字列をテンプレートリテラルに整形してくれるprefer-templateルールも追加しています。
ESLintスタイルガイドで人気の高いairbnbの「eslint-config-airbnb」 は、このprefer-templateがデフォルトで適用されています。
参考:
prefer-template: https://eslint.org/docs/rules/prefer-template
Airbnb JavaScript Style Guide: https://github.com/airbnb/javascript#es6-template-literals

prettier設定

prettier設定を行っている.prettierrc 内容は以下の通り。

.prettierrc
{
  "printWidth": 80
}

printWidth は行内の折り返し文字数を指定するものです。
80文字折り返しを指定していますが、実はprettierのデフォルトも80です。
なので実質このファイルがあってもなくても変わりません。
一応明示的に設定しています。
参考: https://prettier.io/docs/en/options.html#print-width

原因

失敗時の整形処理を順を追って確認すると以下の通りです。

  • prettier整形で三項演算子?部分について、改行位置整形のために一時削除される
  • 複数行に跨がるprettier整形が終わる前に、割り込む形で prefer-template が入ってしまう
  • prettierの1行の文字数折り返し整形を実行しようとするがprefer-templateが対象コード内容を変えているため正しく処理されない

このために三項演算子が整形されず ? が消えたままの現象が発生していました。

▼ 失敗例分析

  • ①で?を削除。
  • ②でprefer-templateが挟まっている。
  • ③でprettierが想定していた形が②で変更されており正しく処理できていない。 code.png

▼ 成功例分析

成功時の場合は、prettier整形がきちんと終った後にprefer-templateが実行されています。

  • ①、②で三項演算子?や1行内折り返し文字数をprettier整形完了。
  • prettier整形後の綺麗な状態で③prefer-templateが正しく実行される。 code2.png

原因検証

prefer-templateなしで実行

"prefer-template": "warn" 部分を削除してみます。
要はテンプレートリテラル整形なしです。
この設定でeslint --fix ./src/index.js を実行。

.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es6: true,
  },
  extends: ["prettier"],
  plugins: ["prettier"],
  rules: {
    "prettier/prettier": "warn",
  },
};

▼ 実行結果

src/index.js
const test1 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";
カテゴリ 結果
test1
test2
test3

実行結果は全部成功。

.prettierrc1行内折り返しを80→120にする

80文字折り返しだと今回のコードでは折り返しが発生します。
そのため120等大きめの数値に設定変更します。
これでeslint --fix ./src/index.js を実行。

.prettierrc
{
  "printWidth": 120
}

▼ 実行結果

src/index.js
const test1 = isFoo ? `データfooを選択中です(${1} 個)` : "選択できるデータは存在しません";

const test2 = isFoo ? `データfooを選択中です(${1} 個)` : "選択できるデータは存在しません";

const test3 = isFoo ? `データfooを選択中です(${1} 個)` : "選択できるデータは存在しません";

カテゴリ 結果
test1
test2
test3

実行結果は全部成功。

三項演算子の?true式を同一行にする

三項演算子の?位置調整の整形がかからないよう全部1行にしてみます。
このESLint設定でeslint --fix ./src/index.js を実行。

▼ 実行前

src/index.js
const test1 = isFoo ? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

const test2 = isFoo ? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


const test3 = isFoo ? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


▼ 実行結果

src/index.js
const test1 = isFoo
  ? `データfooを選択中です(${1} 個)`
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? `データfooを選択中です(${1} 個)`
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? `データfooを選択中です(${1} 個)`
  : "選択できるデータは存在しません";

カテゴリ 結果
test1
test2
test3

実行結果は全て成功。

三項演算子を2行に分けて ?true式を同一行にする

先ほどは三項演算子全部1行にしました。
今度は2行だけど、?true式の間に余計な改行を挟まない状態で試してみます。
このESLint設定でeslint --fix ./src/index.js を実行。

▼ 実行前

src/index.js
const test1 = isFoo 
? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

const test2 = isFoo 
? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


const test3 = isFoo 
? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';


▼ 実行結果

src/index.js
const test1 = isFoo
  ? `データfooを選択中です(${1} 個)`
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? `データfooを選択中です(${1} 個)`
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? `データfooを選択中です(${1} 個)`
  : "選択できるデータは存在しません";

カテゴリ 結果
test1
test2
test3

実行結果は全て成功。

対策

1. 三項演算子の書き方に注意する

JavaScriptにおいては三項演算子の?true式 と同じ行に書く

? の直後に改行を入れても一応動きます。
ですが、大量のコードを一括でESLintした際は、今回の検証のように気づかずコード破損が起こる可能性があります。
そのためJavaScriptの三項演算子レギュレーションとしては、?true式 は同一行に書く方が安全です。

例:1行での書き方

const test1 = isFoo ? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

例:複数行での書き方

const test1 = isFoo
  ? 'データfooを選択中です(' + 1 +' 個)'
  : '選択できるデータは存在しません';

2. ESLint未適用のコードにはprettierをしてからESLintを行う

一度prettier整形してしまえばそれ以降はESLint fixのみでも問題ない。
そのためprettier単独で整形した後に別ステップでESLint fixをかけること。
これなら整形で壊れることはないです。

例えば大量のESlint未適用コードが合った場合、そこに一括ESlintをかけるなら以下1.2.の順序で行う。
こうすれば以降はESlint+prettierを同時にかけても今回の問題は回避できます。

  1. 最初にprettierのみ実行する。
  2. その後別ステップとしてESLint fixをする。

実際にprettilerとESLintを別々にかけてみた

▼ 実行前

src/index.js
const test1 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

const test3 = isFoo ?
  'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';

▼ prettier実行

prettier --write ./src/index.js を実行すると以下になります。

src/index.js
const test1 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? "データfooを選択中です(" + 1 + " 個)"
  : "選択できるデータは存在しません";

三項演算子の?位置やインデントがきちんと整形された状態になりました。
prettier整形成功です。

▼ ESLint実行

prettierをかけたコードに対し、ESLint eslint --fix ./src/index.js を実行すると以下になります。

src/index.js
const test1 = isFoo
  ? `データfooを選択中です(${1} 個)`
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? `データfooを選択中です(${1} 個)`
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? `データfooを選択中です(${1} 個)`
  : "選択できるデータは存在しません";

無事テンプレートリテラル適用されました。
ESLintも成功です。

このように、実行ステップを分ければ、三項演算子を維持したままインデントやスペース、テンプレートリテラルの整形が正しく行えます。

参考:PrettierとESLint fixを分けて実行するケース
https://github.com/prettier/eslint-config-prettier#special-rules

まとめ

いかがでしたでしょうか。
明日のGoodpatch Advent Calendar 2020は、いつもスマイルなフロントエンド @zookeeper08 がお送りします。
皆様よい12月をお過ごしくださいませ。

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

Polyfill.io を使って JavaScript の Polyfill を適用する

この記事は「弁護士ドットコム Advent Calendar 2020」の3日目の記事です。よろしくお願いいたします。


はじめに

JavaScript の実装で Fetch API を使用したい場面がありました。
https://developer.mozilla.org/ja/docs/Web/API/Fetch_API
この機能が Internet Explorer (以下、IE) 非対応である為、何かしらの対応をすることになりました。
(尚、この解説での IE のバージョンは 11 になります。10 以前はほとんど使用されていないので、無視します。)

結果、Polyfill を効かせるライブラリを導入することになり、選定を行いました。

Polyfill とは

JavaScript (ECMAScript) は特定のブラウザやバージョンが古い場合、サポートしていない機能が存在します。Polyfill は、そういったサポートしていない最近の機能を使えるようにするためのコードです。

[参考] Polyfill (ポリフィル) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
https://developer.mozilla.org/ja/docs/Glossary/Polyfill

Polyfill ライブラリの選定する

core-js と Polyfill.io を検討しました。

core-js

https://github.com/zloirock/core-js
実績のある Polyfill ライブラリです。

以前は Babel が提供する @babel/polyfill が core-js を内部的に読み込んでいました。
https://babeljs.io/docs/en/babel-polyfill
因みに、@babel/polyfill は Babel 7.4.0 で非推奨 (deprecated) となっています。

core-js を jsDelivr などの CDN から読み込むことを考えました。
https://www.jsdelivr.com/package/npm/core-js

<script src="https://cdn.jsdelivr.net/npm/core-js@3.7.0/stable/index.js" integrity="sha256-HrF6MoQzzJ82sV7Ql2E5hrTW2iRlrlD54q4o/q9lIFY=" crossorigin="anonymous"></script>

対応は楽なのですが、stable だけに絞ってもファイルサイズが大きくなりページの表示速度が低下する懸念があった為、見送りました。

Polyfill.io

https://polyfill.io
指定したリクエストに応じて、必要とする Polyfill のみを返すサービスです。

ブラウザを判別して必要な Polyfill のみを返すので、Polyfill が不要な主要ブラウザであればファイルサイズは大きくならず、良さそうだと判断したので Polyfill.io について調査と検証を進めることにしました。

Polyfill.io を調査する

今は v3 なので https://polyfill.io/v3/polyfill.min.js です。
v2 を利用し続けているサイトも見かけます。v2 は https://cdn.polyfill.io/v2/polyfill.js です。
今から利用するなら v3 を使いましょう。

.min を削除した URL にアクセスすると詳細が確認できます。
https://polyfill.io/v3/polyfill.js

最新バージョンの Chrome でアクセスすると /* No polyfills needed for current settings and browser */ と出力されます。Polyfill が不要な為、このような出力になります。

CREATE A POLYFILL BUNDLE (url-builder) から、必要な Polyfill を指定できます。
https://polyfill.io/v3/url-builder/

例えば、今回の発端であった Fetch API を適用するには、以下のようになります。

<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch" crossorigin="anonymous"></script>

features などの Querystring については API Reference で説明されています。
https://polyfill.io/v3/api/

こちらを参考に、Querystring に flags=always を指定すると、本来は Polyfill が不要なブラウザからのアクセスでも、適用される内容を確認することができます。
例えば、以下の URL にアクセスすると、適用が不要な Chrome であっても fetch で適用される Polyfill が確認できます。
https://polyfill.io/v3/polyfill.js?features=fetch&flags=always

Polyfill.io を検証する

この時点で Fetch API 以外にも IE 非対応の機能を使用していることに気付き、適用する features について使われている機能を洗い出しました。機能を把握し切れていなかった為、「動作を試してみて、エラーになった機能を一つずつ確認して追加して、また試す」というのを繰り返しました。

途中経過で、以下のような features になりました。

<script src="https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.find%2CEvent%2CNodeList.prototype.forEach%2CPromise%2Cfetch" crossorigin="anonymous"></script>

どうも埒が明かないので、一括で読み込む方針に方向転換しました。
そもそも、ブラウザを判別して必要な Polyfill のみを返してくれるので、一括でも不要な Polyfill は通信されなので問題ないという判断です。

API Reference の features の説明に、以下のような記述がありました。

Omitting or setting to an empty string is equivalent to the value "default", which is an alias for a curated list of polyfills.

features を特に指定しなければ、必要な全ての機能の Polyfill が適用される(これは勘違い、詳しくは後述)と思い、以下のような指定をしました。

<script src="https://polyfill.io/v3/polyfill.min.js" crossorigin="anonymous"></script>

残念ながら、これだと上手く動きませんでした。いくつかの機能が動かずに Polyfill が効いていない挙動になってしまいました。

先の説明をよく読むと分かるのですが、features を指定しない場合は default と同等になります。
https://polyfill.io/v3/polyfill.js?features=default&flags=always
別の記事で「読み込むだけで全ての機能の Polyfill が適用される」というような記述を見掛けたのですが、それは誤りのようです。

どの Polyfill が適用されるかは、CREATE A POLYFILL BUNDLE (url-builder) の各 Polyfill の からも確認できます。
`default` feature で適用される機能の一覧

以上を踏まえて、最終的に以下のような指定になりました。

<script src="https://polyfill.io/v3/polyfill.min.js?features=default%2Ces2015%2CNodeList.prototype.forEach%2CReflect.set%2Cfetch" crossorigin="anonymous"></script>

現段階で必要と判断した以下の features を適用しています。

  • default
  • es2015
  • NodeList.prototype.forEach
  • fetch

defaultes2015 で概ね必要な Polyfill は効いたのですが、個別に NodeList.prototype.forEachfetch を追加しました。

これで最終の動作確認を行いました。

es2015 について

補足になりますが es2015 は ECMAScript 6th edition のことです。 6th edition から発行年が付加されています。つまり es6 と同じです。同様に es2016es7 も同じになります。

es2015es6 は同じなのですが、Polyfill.io だと es2015 を選択した場合は String.fromCodePoint だけ足りないようです。
https://polyfill.io/v3/polyfill.js?features=es2015&flags=always
https://polyfill.io/v3/polyfill.js?features=es6&flags=always

String.fromCodePoint() は ECMAScript 2015 で追加された機能なので本来は含まれるのが正しいと思います。そのうち修正されるかもしれません。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint

現在は年号付きが推奨されている為、今回は es2015 を選択しました。

その結果どうなったか

結果、これでも動作しませんでした。

Object doesn't support property or method 'set' というエラーが発生しました。
これは IE 非対応の FormData.set() を使っていた為でした。
https://developer.mozilla.org/ja/docs/Web/API/FormData/set

FormData.set() は Polyfill.io では対応していません。
https://github.com/Financial-Times/polyfill-service/issues/1205

因みに、core-js でもスコープ外で、他プロジェクトの Polyfill を使うことを勧めています。
https://github.com/zloirock/core-js/issues/482

ここまで進めた結果、今回は「そこまで厳密に IE の動作を求めない」という決着になり、これで対応完了とすることになりました。もし厳密な IE のサポートを求められたら、JavaScript の実装コードを書き直すか、別のライブラリで対応する Polyfill を探すことになるかと思います。

まとめ

Polyfill.io だけで全ての機能に Polyfill が効くわけではないので、Polyfill 非対応の機能など注意して JavaScript を書かないと厳密な IE 対応は厳しそうでした。
別のライブラリを探せば対応する Polyfill はあるとは思いますが、個別に Polyfill を適用していくのも手間ですし、読み込むファイルが増えるとページの表示速度が低下する懸念があります。

そもそも Polyfill.io は SLA が存在しません。
https://polyfill.io/v3/terms/#polyfillio-are-provided-as-is-and-without-any-sla

We do not guarantee that Polyfill.io, or any content on them, will always be available or be uninterrupted. We may suspend or withdraw or restrict the availability of all or any part of Polyfill.io for business and operational reasons.

We do not provide any service level agreement or any other performance commitments for Polyfill.io. We are not able to participate in any technical due diligence or other procurement process that your organisation may have.

厳密な対応が必要なら、外部のサービスや CDN に頼らずに自前で運用する必要があるかもしれません。
Polyfill.io は自前で動かすパッケージも公開しています。
https://polyfill.io/v3/packages/#polyfill-library

どこまで厳密な対応を行うかは、開発や運用コストとの兼ね合いで判断が必要かと思います。

さいごに

既に IE のサポートを終了しているサイトもあるかとは思いますが、一方でまだまだサポートしていかなければいけない状況のサイトもあります。どの程度のサポートを求められるか次第ではありますが、サポートの程度によっては Polyfill.io は選択肢のひとつして、とても良いと思いました。

この記事が皆様の一助となれば幸いです。


さて、明日 (12/4) の「弁護士ドットコム Advent Calendar 2020 | 4日目」は @edi_t さんの「iPhoneX以降のTrueDepthカメラを使ってオンライン会議で簡単にVTuberになる方法(お金がちょっと掛かります)」になります!お楽しみに!

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

JavaScriptでキー入力を拾いたい

はじめに

JavaScript Advent Calendar 2020 17日目の記事です。

「マウスを使わずにキーボードだけで操作して効率UPしているぜ!」という人が一定数いそうなので、Webアプリを作るならキーボードだけでも操作できるようにしようかなと思っています。

作ったWebアプリに...

例えば自作のマークダウンメモアプリに、ショートカットキーで操作する機能を付けたくなったとします。

「WindowsならCtrl + /を、MacならControl + /を押して、"マークダウン記法の一覧"を表示できるようにしたい。」

イベントの取得

何かキーボードのキーが押されたことを検知するのは、とても簡単です。

document.addEventListener('keydown', event => {
    console.log(event);
});

ビューポート内にフォーカスが当たっていれば取得できます。

どのキーが押されたかを知る

手元にApple Keyboard A1843 EMC 3138(JIS配列)がありましたので、どのキーを押してどんな出力が得られるのかを調べてみました。
キーボードの各キーによって取得できるコード
KeyboardEvent.codeの取得結果は以下のSVGファイルに記してあります。

code一覧を見る(外部サイト)

今回の場合は/(スラッシュ)なので、event.code === "Slash"で判定ができます。

さらにWindowsならCtrlキーを、MacならControlキーを共に押していればevent.ctrlKeyがtrueになるため、それも調べます。

試してみる

かなり簡素なコードではありますが、Chromeなどの開発者ツールで手軽に試してみてください。

document.addEventListener('keydown', event => {
    if (event.ctrlKey && event.code === 'Slash') {
        // 処理を書く
        console.log('control + / です');
    }
});

参考リンク

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

JavaScriptで特定のキー入力をトリガーにしたい

はじめに

JavaScript Advent Calendar 2020 17日目の記事です。

「マウスを使わずにキーボードだけで操作して効率UPしているぜ!」という人が一定数いそうなので、Webアプリを作るならキーボードだけでも操作できるようにしようかなと思っています。

作ったWebアプリに...

例えば自作のマークダウンメモアプリに、ショートカットキーで操作する機能を付けたくなったとします。

「WindowsならCtrl + /を、MacならControl + /を押して、"マークダウン記法の一覧"を表示できるようにしたい。」

イベントの取得

何かキーボードのキーが押されたことを検知するのは、とても簡単です。

document.addEventListener('keydown', event => {
    // 変数eventの中身はKeyboardEventオブジェクト
    console.log(event);
});

ビューポート内にフォーカスが当たっていれば取得できます。

キーの識別に使えるKeyboardEventのプロパティ

  • key : ShiftキーやOptionキーによる変化を考慮して、キーを現す文字列を返します。
  • code : 押されたキーそのものを現す文字列を返します。ただしJIS配列のキーボードでは、[の入力に対し"BracketRight"が出力されるなど直感的ではありません。今回はこれを使います。
  • keyCode : 非推奨。キーを現す一意な数値を返します。
  • which : 非推奨。keyCodeと同じ値が出力されます。
  • charCode : 非推奨。2020年12月17日時点で、ChromeやFirefox、Mac Safariではどのキーを押しても0が出力されます。

どのキーが押されたかを知る

手元にApple Keyboard A1843 EMC 3138(JIS配列)がありましたので、どのキーを押してどんな出力が得られるのかを調べてみました。
キーボードの各キーによって取得できるコード
KeyboardEvent.codeの取得結果は以下のSVGファイルに記してあります。

code一覧を見る(外部サイト)

今回の場合は/(スラッシュ)なので、event.code === "Slash"で判定ができます。

さらにWindowsならCtrlキーを、MacならControlキーを共に押していればevent.ctrlKeyがtrueになるため、それも調べます。

試してみる

かなり簡素なコードではありますが、Chromeなどの開発者ツールで手軽に試してみてください。

document.addEventListener('keydown', event => {
    if (event.ctrlKey && event.code === 'Slash') {
        // 処理を書く
        console.log('control + / です');
    }
});

参考リンク

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

ReduxとContextの再レンダリングの違いをサンプルアプリケーションと共に確認する

VISITS Technology株式会社( https://visits.world/ )の2020年アドベントカレンダー1発目を努めさせていただきますpipopotamsauです。

https://qiita.com/advent-calendar/2020/visits

業務ではフロントエンドエンジニアとしてReactを用いたアプリケーションを開発していますので、React関連について書きたいと思います。

1. ReduxとContextについて

Reactアプリケーションでデータ、特にグローバルなデータをストアしたい場合Reduxを用いるか、はたまたContextを使うかという議論があるかと思います(最近はHTTP Requestをキャッシュするライブラリなどが出てきて色々選択肢が増えましたが1、本記事の本筋とは関係ないため言及しません)。

どちらを使うべきであるかは開発の前提条件にもよりますので一概には言えませんが、論点の中には「パフォーマンス」に関するものがあります。

2. パフォーマンスについての議論

おそらく「Redux vs Context」でググると、「頻繁にグローバルストアを更新するんならReduxの方がパフォーマンスいいよ」的なものが出てくると思います。これは何故でしょうか?

これは両者の状態が更新されたときに発生する再レンダリングのスコープが違うからです。

Redux: 変更された値をサブスクライブしているコンポーネントのみ再レンダリングが走る
Context: 変更されたContextのProviderの子コンポーネント以下全てに再レンダリングが走る

このような違いがあるため「頻繁にグローバルストアを更新するんならReduxの方がパフォーマンスいいよ」という話が出てきます。

3. ReduxとContextの再レンダリングの違いをサンプルアプリケーションで確認してみる

さて、ここからが本番です。
上記でどのような再レンダリングの違いがあるかを言葉で書きましたが、今度は実際にサンプルアプリケーションで確認していきましょう。

※ 本記事のサンプルコードのレポジトリはこちらです

前提として、ReduxとContextそれぞれのサンプルアプリケーションで以下のようなグローバルステートを保持するとします。

{
  user: { id: 1, name: 'test name' },
  posts: [{ id: 1, title: 'test post' }]
}

上記のステートを更新したときに、どのような再レンダリングの違いがあるかをみていきます。

Reduxの再レンダリング

前述したグローバルステートを持った、以下のようなReduxを用いたサンプルアプリケーションを作成しました。

redux-sample-application.png

コードとしてはこのようになります。

import { Provider, useSelector, useDispatch } from 'react-redux';
import { store } from '../reduxStore';
import classes from '../styles/page.module.css';

function UserInfo () {
  console.log('UserInfo is updated!');
  const user = useSelector(state => state.user);
  const dispatch = useDispatch();

  return (
    <div className={classes.userContainer}>
      <p>{user.name}</p>
      <button
        onClick={() => {
          dispatch({ type: 'UPDATE_NAME', payload: 'updated name' });
        }}
      >
        update user
      </button>
    </div>
  )
}

function PostList () {
  console.log('PostList is updated!');
  const posts = useSelector(state => state.posts);
  const dispatch = useDispatch();

  return (
    <div className={classes.postsContainer}>
      <ul>
        {
          posts.map(post => <li key={post.id}>{post.title}</li>)
        }
      </ul>
      <button
        onClick={() =>  {
          dispatch(
            { type: 'ADD_POST', payload: { id: posts.length + 1, title: 'added post' }}
          )
        }}
      >
        add post
      </button>
    </div>
  )
}

export default function ReduxExample () {
  return (
    <Provider store={store}>
      <h1>Redux</h1>
      <UserInfo />
      <PostList />
    </Provider>
  )
}

親コンポーネントとしてReduxExampleコンポーネントがあり、ReduxのstoreをProvideしています。
また、子コンポーネントとしてuserを参照しているUserInfoコンポーネントとpostsを参照しているPostListコンポーネントがあります。

このような構成でuserpostsの値を更新/追加するとどうなるでしょうか?

redux-demo.gif

userを更新した際はUserInfoコンポーネントのみ再レンダリングされ、postsを更新した時はPostListコンポーネントのみ再レンダリングされます。

Contextの再レンダリング

今度はContextについてみていきます、以下のようなContextを用いたサンプルアプリケーションを作成しました(Reduxのものと見た目は一緒です)。

context-sample-application.png

コードとしてはこのようになります。

import { useContext, useReducer } from 'react';
import { initialState, reducer, Context } from '../context';
import classes from '../styles/page.module.css';

function UserInfo () {
  console.log('UserInfo is updated!');
  const { user, dispatch } = useContext(Context);

  return (
    <div className={classes.userContainer}>
      <p>{user.name}</p>
      <button
        onClick={() => dispatch({ type: 'UPDATE_NAME', payload: 'updated name' })}
      >
        update user
      </button>
    </div>
  )
}

function PostList () {
  console.log('PostList is updated!');
  const { posts, dispatch } = useContext(Context);

  return (
    <div className={classes.postsContainer}>
      <ul>
        {
          posts.map(post => <li key={post.id}>{post.title}</li>)
        }
      </ul>
      <button
        onClick={() =>  {
          dispatch(
            { type: 'ADD_POST', payload: { id: posts.length + 1, title: 'added post' }}
          )
        }}
      >
        add post
      </button>
    </div>
  )
}

export default function ContextExample () {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <Context.Provider value={{ user: state.user, posts: state.posts, dispatch }}>
      <h1>Context</h1>
      <UserInfo />
      <PostList />
    </Context.Provider>
  )
}

親コンポーネントとしてContextExampleコンポーネントがあり、ContextのステートをProvideしています。
また、子コンポーネントとしてuserを参照しているUserInfoコンポーネントとpostsを参照しているPostListコンポーネントがあります。

Reduxのサンプルアプリケーションと同様に、こちらもuserpostsの値を更新/追加してみましょう。

context-demo.gif

Reduxとは違い、userpostsのうち片方を更新すると、UserInfoコンポーネントとPostListコンポーネントの両方が再レンダリングされていることがわかります。

userを更新したのにPostListコンポーネントが再レンダリングされてしまう、もしくはその逆のパターンの抑制策としてはReact.memoを用いmemo化を行う方法があります。本記事では詳しく説明しませんが、もし興味がある方は以下を確認してみてください。

https://ja.reactjs.org/docs/react-api.html#reactmemo

4. 終わりに

以上のように今回はReduxとContextの再レンダリングの違いについて、文字だけでなく実際にサンプルアプリケーションを用いて確認していきました。

本記事のサンプルコードは以下になります。コード全体がみたいという方はこちらを参照してください。
https://github.com/pipopotamasu/redux-context-comparison

明日は弊社のEM兼バックエンドエンジニアであるhamがお送りします。

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

[TypeError: Converting circular structure to JSON] JSON.stringify()の時にエラーが出る

コード

const params = {
  user_id: firebase.auth().currentUser.uid,
  id_token: firebase.auth().currentUser.getIdToken(false), // await が必要
}

const body = JSON.stringify(params) // エラー発生

エラー

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'C'
    |     property 'c' -> object with constructor 'C'
    |     property 'b' -> object with constructor 'zc'
    --- property 'a' closes the circle
    at JSON.stringify (<anonymous>)
    at _callee4$ (common.js?4edf:57)
    at tryCatch (runtime.js?96cf:63)
    at Generator.invoke [as _invoke] (runtime.js?96cf:293)
    at Generator.eval [as next] (runtime.js?96cf:118)
    at asyncGeneratorStep (asyncToGenerator.js?1da1:3)
    at _next (asyncToGenerator.js?1da1:25)
    at eval (asyncToGenerator.js?1da1:32)
    at new Promise (<anonymous>)
    at Object.eval (asyncToGenerator.js?1da1:21)

原因

awaitの付け忘れ。

解決コード

const params = {
  user_id: firebase.auth().currentUser.uid,
  id_token: await firebase.auth().currentUser.getIdToken(false), // awaitをつけた
}

const body = JSON.stringify(params)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

焼き芋屋の場所をシェアするスマホサイト(半アプリ)を作った。

焼き芋が食べたい!移動販売の!

私の小さいころは、
よくトラックの焼き芋屋さんを見かけました。
焼きたては、とても美味しかった!
焼きたてだから、美味しかったと思います。
でも、
最近は見かけなくなりました。

スマホで探せるか?

探してみました。
・焼き芋専門店は見つかるけど、遠い・・・
  (最近増えた?)
・移動販売の弁当屋さんターゲットのはあるけど、焼き芋はない
  (見つけられないだけ?)
 

じゃあ作ろう、でも・・・・

C系の仕事が中心だったのでWeb系の技術はほとんど、
Wordpressをつかって、HPを作るのは出来るが、
それ以外は致命的にやってこなかった。
html直書きが普通でしょ?
cssってなに?
javascriptは訪問カウンタ?
ほんと、
前世紀で止まっている(笑)

どんなのにしようか?

じゃらんやホットペッパーみたいな、「お店が掲載する」みたいなのはめんどくさい。
ー>店主でもファンでもいいので地図上に「場所を投稿」してもらうサイトにしよう。
移動販売しているのを探せるようなのがいい
->投稿情報に掲載期限を設けよう(当日のみがいい)
専門店も探せないと
->こっちは投稿情報に掲載期限ナシに
コンビニやスーパーあるよ
->専門店とは種類をわけて置こう

だいたい、骨子は固まった。

ついでに、パート1「グーグルマップを使う」

ホームページで地図は普通に見るのだが、
使ったことはなかった。
しかも「グリグリ動く地図」だから、
javascriptで、、、勉強しなおしだ。

ついでに、パート2「PWAってやつ、なんか便利そう」

スマホアプリを作りたいと思ったのだが、
さすがに色々と知識が足りなさ過ぎ、、、
で、見つけたのが、
PWA
「ホームページを作るノリで
 モバイルアプリっぽく出来る
 android,ios両対応」

おお、これは俺にとってすごく助かる

で、、、できた。やっと、できた ∈( ´_______` )∋

詳細は省くが、苦節10ヶ月かかった。
去年の焼き芋シーズン終わりから初めていたのだが、
既に今年のシーズンが始まってしまった。。。

他にも色々やっていたのも悪いのだが、
系統だって説明してくれるHPがうまく見つけられず
バージョンの違うAPIの説明HPに惑わされ、四苦八苦しました。

で、サイトは

こちらです。
[焼き芋屋さんの場所をみんなでシェア]
https://app.yakiimoshop.com/?ads=32

screanshot_samsung-galaxynote5-white-portrait.png
こんな感じに、なりました。
先にコンビニ(ローソン100)の情報は載っけたので、寂しい画面ではないように出来ました。
焼き芋専門店も見つけては載せていってます。
今のところ、ツイッターで見つけた情報とかを追加追加してます。

現状、というかサイトの出来は、、、

技術的には、とても拙いものです
恥さらしじゃないかみたいなサイトです(ワラ

でも、とりあえずやりきりました。
それだけで、今のところ満足 <(`^´)> エッヘン
ちょっとずつ、改善予定。

焼き芋屋さんの情報、求む

ぜんぜん足りてません。
すっかすかな地域がたくさんあります。
見つけたら、投稿して欲しいです。
(店主からの投稿、せつに望みます)

正直
サイトが出来上がってから、
ネットで焼き芋屋さんがどこにいるか調べましたが(今も調べ続けてます)
焼き芋屋さん、すくなっ
と思ってます。
専門店が出来たと、聞いたりするのですが、
他のスイーツに比べれば、圧倒的に少ない気が・・・・
(一つの町に1つ、あるかどうか?)

結局、
私の住所の近くには、なかなか見つからず、
美味しい焼き芋、今年はまだ、食べれていない。。。
(すみません、技術情報、特にないです)

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

【Next.js】getServerSidePropsでクエリパラメータを取得する方法

前回、Next.jsを使ったクライアントサイドでのクエリパラメータを取得する方法を解説しました。

Next.jsでクエリパラメータを遷移先に渡すにはどうすればいいのか? - Qiita

前回使用したuseRouterフックはクライアントサイドで動きますが、サーバーサイドではエラーになります。そのためサーバーサイドでクエリパラメータを取得するには別の方法が必要です。

なので、今回はサーバーサイドでクエリパラメータを取得する方法を解説します。

サーバーサイドレンダリングでクエリパラメータを取得する方法

まずはサーバーサイドでレンダリングするためgetServerSidePropsの定義が必要です。その際の注意点としてはexportayncキーワードをつけることです。これにより、リクエスト毎にこのページを事前にレンダリングします。

そして、このgetServerSidePropsの中でuseRouterを使おうとするエラーが発生します。

WS000002.JPG

これはサーバーサイドレンダリングではuseRouterを使えないためです。クライアントサイドで使う前提になっています。

ここで使えるのがgetServerSidePropsの引数です。通常contextという名前で設定しますが、どんな引数名でも使えるはずです。

このcontextの中には

  • req:HTTPリクエストオブジェクト
  • res:HTTPレスポンスオブジェクト
  • query:クエリパラメータ

が入っています。この中のqueryの中にクエリパラメータの値が格納されているので、この値を使えばサーバーサイドレンダリングでも使うことができます。

たとえば、次のページを作る際には2つのページが必要になります。

Nextjsのパラメータ引き渡し(サーバーサイドレンダリング).gif

  1. クエリパラメータを送るページ
  2. クエリパラメータを受け取るページ

クエリパラメータを受け取るページでは、受け取った値でWebAPIを呼び出してGoogleブックの検索結果を表示しています。

クエリパラメータを送るページ

index.jsx
import { useRouter } from 'next/router';
import {useState} from 'react';

export default function Index() {
  const router = useRouter();                //ルーターの取得
  const [keyword, setKeyword] = useState();  //検索キーワード

  // ボタンをクリックしたときの処理
  const clickButton = () => {
    //未入力の時
    if (!keyword) {
      return;
    }

    router.push({
        pathname:"/result",       //URL
        query: {keyword :keyword} //検索クエリ
      });
  }

  return (
    <div style={{textAlign: "center", marginTop: "50px"}}>
      {/* 入力項目 */}
      <input 
        type="text" 
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)} /*変更時keywordに値をセット*/
      />

      {/* ボタン */}
      <button 
        onClick={clickButton}
        disabled={!keyword}>    {/*入力項目が未入力の場合、非活性*/}
        検索
      </button>
    </div>
  )
}

クエリパラメータを送信するページではuseRouterフックを使って画面遷移とクエリパラメータの設定を行っています。router.pushメソッドの引数にURLとクエリパラメータをセットしています。

  • pathname:URL
  • query:クエリパラメータ

注意点としてはクエリパラメータは連想配列で設定します。複数セットする可能性があるためです。

クエリパラメータを受け取るページ

//サーバーサイドレンダリング
export async function getServerSideProps(context) {
  //検索キーワード
  const keyword = context.query.keyword;

  //Googleブックで検索
  const response = await fetch (encodeURI(`https://www.googleapis.com/books/v1/volumes?q=${keyword}`));

  return {
    props: {
      bookList: await response.json(),  //Googleブックの戻り値
      keyword: keyword,                 //検索キーワード
    }
  }
}

export default function Result(props) {

  //Googleブックの情報を取得
  const bookList = props.bookList.items.map(item => item.volumeInfo).map(((item) => {
    return {
      title : item.title,                             //タイトル
      url   : item.infoLink,                          //URL
    }
  }));

  return (
    <div style={{marginLeft: "50px", marginTop: "50px"}}>
      <h1>検索キーワード:{props.keyword}</h1>

      {/* 検索データの表示 */}
      <ul>
        {bookList.map((item) =>
          <li>
            <a href={item.url}>{item.title}</a>
          </li>
        )}
      </ul>
    </div>
  )
}

一方、遷移先ページではgetServerSidePropsメソッド内でクエリパラメータを取得してgoogleブックで検索した結果をリストで表示しています。

クエリパラメータはcontext.query.keywordで取得しています。keywordの部分は遷移元で設定した名前です。

後は、getServerSidePropsの戻り値のpropsにクライアントサイドに渡したい値をセットして表示するだけです。クライアント側でbookList変数で取得した検索結果を加工して、map関数を使ってリスト表示しています。

まとめ

  • router.pushpathnameに遷移先URL、queryにクエリパラメータをセットする
  • getServerSidePropsを使うことでサーバーサイドレンダリングを利用できる
  • 引数のcontext.query内に遷移元から送られたクエリパラメータが取得できる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

憧れのギニュー特戦隊の誰に似てるか判定するLINEbotを作ったから、ぜってぇ見てくれよなっ!

ギニュー特戦隊に入りたい

皆さんも(特に男性なら)、人生で一度はギニュー特戦隊に入隊したいと思いましたよね?
今回はその願いを少しでも叶えるべく、次のようなボットを作成しました。
* まさかそんな人はいないと思いますが、ギニュー特戦隊を知らない方はこちらのwikipediaをご確認ください。

早速使ってみたい方は、一番下にQRコードを貼っておきますので、ご利用ください。
ちなみに、万が一、リクームとジース以外の方に似ていると判定されたら、その方は人間(ホモ・サピエンス)という概念を超えている方ですので、すぐにご連絡ください!

開発環境の下準備

1) VScodeのインストール
VScodeのインストールついては、googleなどで他の記事を検索してください。
2) node.jsとnpmのインストール
Macでの環境作りは、こちらの別の記事にまとめてあります。
参考にしてください。
Macにnode.js,npmのインストール方法

開発環境の準備

1) ginyuforceというフォルダを作成
2) VSCodeで上記フォルダを開き、フォルダ内にginyuforce.jsを作成
3) VSCode内でターミナルを開き、フォルダをnpm管理できるように初期化

terminalコマンド
$ npm init -y

システムの概要

スクリーンショット 2020-11-29 17.11.59.png

Teachable Machineによる画像認識AIの作成

以前の記事(誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた。)では画像判定AIをAIメーカーで作成したので、今回はTeachable Machineを使用し、画像判定AIを作成しました。
スクリーンショット 2020-11-29 14.57.23.png

* 使い方については、こちらの記事>【備忘録】Teachable Machineの利用方法が参考になりました。ぜひ一度お読みください。(よかったらLGTMをお願いします。)

ginyuforce.jsのコード

ginyuforce.js
'use strict'; // おまじない

const { JSDOM } = require('jsdom');
var dom = new JSDOM('');
global.document = dom.window.document;
global.HTMLVideoElement = dom.window.HTMLVideoElement;
const canvas = require('canvas');
global.fetch = require('node-fetch');
const tmImage = require('@teachablemachine/image');

const express = require('express');
const line = require('@line/bot-sdk');
const fs = require('fs');

const PORT = process.env.PORT || 3000;

const config = {
    channelSecret: '自身が作成したLINEbotのシークレットトークンを入力',
    channelAccessToken: '自身が作成したLINEbotのアクセストークンを入力',
};

// Teachable Machine
let model;

// https://teachablemachine.withgoogle.com/
// ここでエクスポート、クラウドにモデルをアップロードした後に取得できる
const URL = '作成したモデルのTeachable Machineのリンクを入力';

// ########################################
//  Teachable Machineを使って画像分類をする部分
// ########################################

// 初期化が時間かかるので、node立ち上げ時に行うようにする
async function initTeachableMachine() {
  const modelURL = URL + 'model.json';
  const metadataURL = URL + 'metadata.json';
  // モデルデータのロード
  model = await tmImage.load(modelURL, metadataURL);

  // クラスのリストを取得
  // const classes = model.getClassLabels();
  // console.log(classes);
}
initTeachableMachine();

async function predict(imgPath) {
  // canvasに画像をロードする
  const image = await canvas.loadImage(imgPath);

  // 判定する
  const predictions = await model.predict(image);

  // 一番近いもの順でソート
  predictions.sort((a, b) => {
    return b.probability - a.probability;
  });

  return predictions;
}

// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
  // 受信したWebhookが「画像以外」であればnullを返すことで無視します
  if (event.message.type === 'image') {
    console.log("画像が送られてきた");

    // 画像を保存
    const downloadPath = './01.png'; //左記の名前で使用中のフォルダ内に送信された画像が保存される
    const getContent = await downloadContent(event.message.id, downloadPath);

    const result = await predict(getContent);

    // AIメーカーAPIの結果から、返信するメッセージを組み立てる
    let text = '';
    let name = '';
    name = result[0].className
    // 判定結果をテキストに代入
    text = 'あなたは『' + name + "』に最も似ています!";
    // これまでの結果を確認するためにコンソールに表示
    console.log(result);
    console.log(name);
    console.log(text);
    // 判定結果に応じた画像を送信
    if(name === 'ジース'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        },{
            type: 'image',
            originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/gisu.jpg', 
            previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/gisu.jpg'
        }]);
     }else if(name === 'リクーム'){
        return client.replyMessage(event.replyToken,[{
            type: 'text',
            text: text
        },{
            type: 'image',
            originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/rikumu.jpg', 
            previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/rikumu.jpg'
        }]);
     }else if(name === 'グルド'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        },{
            type: 'image',
            originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/gurudo.jpg', 
            previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/gurudo.jpg'
        }]);
     }else if(name === 'バータ'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        },{
            type: 'image',
            originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/bata.jpg', 
            previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/bata.jpg'
        }]);
     }else if(name === 'ギニュー隊長'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        },{
            type: 'image',
            originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/ginyu.jpg', 
            previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/ginyu.jpg'
        }]);
    }
  }

  // 「テキストメッセージ」であれば、受信したテキストをそのまま返事します
  if (event.message.type === 'text') {
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: event.message.text // ← ここに入れた言葉が実際に返信されます
    });
  }
}

// ########################################
//          LINEで送られた画像を保存する部分
// ########################################

function downloadContent(messageId, downloadPath) {
  const data = [];
  return client.getMessageContent(messageId)
    .then((stream) => new Promise((resolve, reject) => {
      const writable = fs.createWriteStream(downloadPath);
      stream.on('data', (chunk) => data.push(Buffer.from(chunk)));
      stream.pipe(writable);
      stream.on('end', () => resolve(Buffer.concat(data)));
      stream.on('error', reject);
    }));
}

// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP GETによって '/' のパスにアクセスがあったときに 'Hello LINE BOT! (HTTP GET)' と返事します
// これはMessaging APIとは関係のない確認用のものです
app.get('/', (req, res) => res.send('<h1>Hello LINE BOT! (HTTP GET)</h1>'));

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
  // Webhookの中身を確認用にターミナルに表示します
  console.log(req.body.events);

  // 空っぽの場合、検証ボタンをクリックしたときに飛んできた"接続確認"用
  // 削除しても問題ありません
  if (req.body.events.length == 0) {
    res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します
    console.log('検証イベントを受信しました!'); // ターミナルに表示します
    return; // これより下は実行されません
  }

  // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
  // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
  Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// (環境によってはローカルネットワーク内にも公開されます)
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);

実行に必要なパッケージのインストルール

下記のコードを入力し、実行に必要なパッケージを入力する。

2) Teachable Machineを動かすのに必要なパッケージ

terminalコマンド
$ npm i @line/bot-sdk express

1) LINEbotを動かすのに必要なパッケージ

terminalコマンド
$ npm i @teachablemachine/image @tensorflow/tfjs canvas jsdom

Herokuへのデプロイ

node.jsが動作する無料のクラウドサーバーであるHerokuにデプロイします。

下準備

1) Heroku CLIのインストール
https://devcenter.heroku.com/articles/heroku-cli

2) Herokuアカウントの作成
https://signup.heroku.com/

デプロイ

ターミナルで下記コマンドを実行します。

terminalコマンド
$ heroku login

実行するとブラウザが自動的に起動するので、ログインします。
すると、ターミナル上でもログインが完了します。

その後、package.jsonの"scripts"の中に、"start"を下記のように追記し保存します。

package.json
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node ginyuforce.js"
  },

Heroku内にデプロイ先のディレクトリを作成します。
下記の順でターミナルにコマンドを入力します。

terminalコマンド
$ git init
terminalコマンド
$ heroku create 名付けたいデプロイ先の名前 //付けたい名前がなければ入力なしでもOK

Heroku内にデプロイ先のディレクトリが作成されると、ターミナルに下記のように表示されます。

terminal
Creating app... done, ⬢ デプロイ先の名前
https://デプロイ先の名前.com/ | https://git.heroku.com/デプロイ先の名前.git

このデプロイ先ディレクトリに、作成したディレクトリを入れていきます。
下記の順でターミナルにコマンドを入力します。

terminalコマンド
$ git add .
terminalコマンド
$ git commit -m 'init'
terminalコマンド
$ git push heroku master

最終的に下記のように表示されれば完了です。

terminal
remote:        https://デプロイ先の名前.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/デプロイ先の名前.git
 * [new branch]      master -> master

これでデプロイ完了です。

ファイルを更新する時

まず、更新するファイルが入ったディレクトリがherokuのデプロイ先のディレクトリと紐づいているかを確認します。

terminalコマンド
$ git remote -v

紐づいている場合

紐づいている場合は、ターミナルに下記のように表記されます。

terminal
heroku https://git.heroku.com/heroku上のデプロイ先の名前 (fetch)
heroku https://git.heroku.com/heroku上のデプロイ先の名前 (push)

紐づいていない場合

紐づいていない場合は、ターミナルに下記のように表記されます。

terminal
fatal: not a git repository (or any of the parent directories): .git

その場合は、ターミナルに下記の順に入力し、空のディレクトリを作成し、heroku上のデプロイしたいディレクトリと紐付けを行います。

terminalコマンド
$ git init //空のディレクトリを作成
terminalコマンド
$ heroku git:remote -a heroku上のデプロイしたいディレクトリ名 
  //herokuのデプロイ先と先ほど作成した空のtディレクトリを紐付け

その上で、下記の順にコマンドを入力し、ファイルを更新します。

terminalコマンド
$ git add .
terminalコマンド
$ git commit -m 'upd'
terminalコマンド
$ git push heroku master

LINE Developersからbot作成

1) LINE Developersにアクセスし、LINEアカウントでログイン
2) プロバイダー作成
3) 新規チャンネルの作成
4) チャネルアクセストークンとチャネルシークレットの取得
1)〜4)までは下記を参考に進みました。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest
5) webhookの設定
Herokuから取得したデプロイ先のURLを入力。
/webhookをつけることを忘れずに。
スクリーンショット 2020-11-22 12.20.39.png

QRコード

完成したギニュー特戦隊判定LINEbotのQRコードがこちらです。
スクリーンショット 2020-11-29 17.01.09.png

どうでしたか皆さん?
ギニュー特戦隊に成る願いは叶えらましたか??
神龍.jpg

その他の記事

近すぎると小池都知事が『密です。』と連呼するデバイスを作ったら腹筋が崩壊したので、皆さんにも試して欲しい。

誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた。

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

地図の四隅にボタンを追加する

Mapbox の地図の左上には、拡大・縮小ボタンなどが表示されています。
ここに任意のボタンを追加してみます。

image.png

// Control implemented as ES6 class
class HelloWorldControl {
  onAdd(map) {
    this.map = map;

    const homeButton = document.createElement('button');
    homeButton.innerHTML = '<i class="fa fa-home" aria-hidden="true"></i>';
    homeButton.addEventListener('click', (e) => {
        alert('Home button click');
    });

    this.container = document.createElement('div');
    this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';
    this.container.appendChild(homeButton);

    return this.container;
  }

  onRemove() {
    this.container.parentNode.removeChild(this.container);
    this.map = undefined;
  }
}

const opt = {
  container: 'map',
  style: {
    version: 8,
    sources: {
      OSM: {
        type: "raster",
        tiles: [
          "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
        ],
        tileSize: 256,
        attribution:
        "OpenStreetMap",
      },
    },
    layers: [{
      id: "BASEMAP",
      type: "raster",
      source: "OSM",
      minzoom: 0,
      maxzoom: 18,
    }],
  },      
};

opt.center = [138.73072, 35.36286];
opt.zoom = 13;
const map = new mapboxgl.Map(opt);

map.addControl(new mapboxgl.NavigationControl({
  showCompass: false,
}));

map.addControl(new HelloWorldControl(), 'top-right');

HelloworldControl というボタンを示すクラスを実装し、そのインスタンスを map.addControl() します。
HelloworldControl の実装での注意点は、コンテナとなる div 要素に mapboxgl-ctrl mapboxgl-ctrl-group のクラスを設定してあげることです。
これを行うと、ボタンの見た目が、拡大・縮小などと同じになります。
逆に見た目を完全に独自にしたい場合はこれを行わず、自分でデザインすることになります。

ボタンの表示位置は画面の四隅(top-right, bottom-right, bottom-left, top-left)が選べます。
位置の細かい指定はできず、地図隅からのマージンも指定できません。
恐らくですが、HelloworldControl の実装方法次第では、隅からの余白を空けることはできるのだとおもいます。

「ボタンが押された」ことの検知も HelloworldControl の実装で行います。Mapbox がサポートできる事はないです。

参考

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

アレクサとTodoistでやることリスト・お買い物リスト

Todoistって、Amazon Echoと連携できるんですね!
しかもWebAPIが充実しているので、さらに連携の輪が広がりそうです。

ちなみに、Todoistは、タスク管理ツールです。一般には、やることリストとか、買い物リストが挙げられますが、メモ的に使えて、かつ、期限を設定して予定を立てたり、失念するのを避けるのに役立ちます。

todoist
 https://todoist.com/

今回作成する全体の構成はこんな感じです。

image.png

すでに、todoistはAlexaとの連携をサポートしていますし、AndroidやiPhone用のアプリもありますので、その部分は特に難しいところはありません。
今回は、やることリスト・買い物リストの表示をWebページとして表示します。todoistはオフィシャルでWebAPIが充実していますし、npmモジュールもあるので、活用の幅が広がります。
また、さらに、PWA化することでネイティブアプリ化し、それをAndroidで固定アプリに設定することで、やることリスト表示の専用機に仕立て上げます。

毎度のことですが、ソースコードもろもろをGitHubに上げておきました。

poruruba/todolist
 https://github.com/poruruba/todolist

準備

todoistのアカウント登録

まずは、以下のURLから、todoistのアカウント登録をします。

todoist
 https://todoist.com/ja

image.png

「はじめる」ボタンまたは右上のサインアップをクリックします。

image.png

ここでは、スマートフォンにインストールしているAmazon Alexaアプリに設定しているアカウントでサインアップし、認証を完了させます。

image.png

これでアカウントが登録されました。

todoistアプリをインストール

Google Playから、Todoistをインストールします。

image.png

todoistにアカウント登録したときのアカウントでログインします。

image.png

ログインが完了しました。

image.png

Alexa連携を設定

すでに、スマートフォンに、「Amazon Alexa」がインストールされている前提で進めます。
以降は、Androidでの操作です。

まずは、下の方の「その他」→「設定」→「リスト」を選択します。

image.png

そうすると、Any.do、AnyList、Todoistが表示されていますので、Todoistの右側の⊕をタッチします。

image.png

以下のように表示されるので、「有効にして使用する」ボタンを押下します。

image.png

リストへのアクセス権の確認が表示されますので、「アクセス権を保存」ボタンを押下します。

image.png

そうすると、todoistのサイトに飛んで、Agreeするかが聞かれますので、「Agree」ボタンを押下します。

image.png

これで、連携設定が完了しました。

image.png

もう一度、todoistアプリの方を開いてみましょう。
そうすると、左上の三のマークをタッチし、プロジェクトのところをタッチすると、「Alexa ToDo リスト」というのと、「Alexaの買い物リスト」が増えているのがわかります。

image.png

最後に、この「Alexa ToDo リスト」と「Alexaの買い物リスト」をお気に入りにしておきます。
「プロジェクト」→「プロジェクトを管理」をタッチし、「Alexa ToDo リスト」と「Alexaの買い物リスト」の右側にあるハートマークをタップし、赤色にします。これによって、この2つがお気に入りのプロジェクトとなりました。この意味はあとで、わかります。

image.png

あと、お好みで、todoistアプリをAndroidのホーム画面にウィジェットとして登録してもよいかと思います。

とりあえずAlexa+todoist連携を試してみる

以下に、発話例がありますので、試してみましょう。

Amazon Alexa で Todoist を使う
 https://get.todoist.help/hc/ja/articles/360010721059-Amazon-Alexa-%E3%81%A7-Todoist-%E3%82%92%E4%BD%BF%E3%81%86

例えば以下をAmazon Echoに話しかけてみましょう。

・アレクサ、やることリストに洗濯を追加して。
・アレクサ、今日のやることリストは?
・アレクサ、やることリストの洗濯を完了にして
・アレクサ、買い物リストに納豆を追加して
・アレクサ、今日の買い物リストは?
・アレクサ、買い物リストの納豆を完了にして

めでたく、こんな感じで追加されました。

image.png

Node.jsサーバからtodoistのリストを取得する

たいていの方は上記まででよいのですが、勉強をかねて、拡張に挑戦します。
Node.jsからの操作には、以下のnpmモジュールを利用させていただきました。

romgrk/node-todoist
 https://github.com/romgrk/node-todoist

以下も使っています。

node-fetch/node-fetch
 https://github.com/node-fetch/node-fetch

リスト取得する前に、ユーザごとにAPIトークンの取得が必要です。

todoist Developer: Authorization
 https://developer.todoist.com/sync/v8/#authorization

OAuthに似たやり取りで、ブラウザ側とサーバ側での連携が必要です。

まずは、サーバのURLをtodoistに登録しておく必要があります。以下のURLを開いて、「Create a new app」ボタンを押下します。

App Management Console
 https://developer.todoist.com/appconsole.html

image.png

App display nameには適当な名前を、App service URLには、これから立ち上げるNode.jsのWebページのURLを指定しておきます。

次に作成したappを選択肢、OAuth redirect URLを指定します。後で作成するのですが、立ち上げるNode.jsのWebページと同じにしておきます。(Single Page Applicationとして実装するため)。最後に、「Save settings」ボタンを押下します。

image.png

その時に表示される、「Client ID」「Client secret」を覚えておきます。後で使うので。

①サーバ側に、ユーザ識別子.jsonというファイルを作成しておきます。
これがばれてしまうと、ログインされてしまいますので、ユーザ識別子は、推測されにくいランダムな値にしましょう。

②ブラウザからClient IDを指定して、ログインを開始する。

以下のように形成されるURLにジャンプします。

https://todoist.com/oauth/authorize?client_id=" + TODOIST_CLIENT_ID + "&scope=data:read&state=" + value

TODOIST_CLIENT_IDは、先ほど覚えておいたClient IDです。stateには正しくは乱数等推測されにくいものにするのですが、手を抜いていて、ユーザの識別子を指定します。①の通り、このユーザの識別子の名前+.jsonで、サーバ側にファイルが作成されている前提です。

③todoistのログインページが表示されるので、ログインする。

todoistのアカウント登録時に使った認証アカウントでログインします。

④認可コードを取得する

ログインが完了すると、OAuth redirect URLで指定したURLにジャンプしてきます。その時に、認可コードとstateが返ってきます。
stateは、②で指定した値のはずです。

⑤認可コードからAPIトークンを取得する

認可コードを取得したので、これを使って以降のtodoistのWebAPI呼び出しに必要なAPIトークンを取得します。子の取得には、Client secretが必要です。秘匿の値として扱う必要があるので、Nodeサーバに渡して、サーバ側で実施します。

api/controllers/todoist/index.js
exports.handler = async (event, context, callback) => {
    var body = JSON.parse(event.body);
    var apikey = event.requestContext.apikeyAuth.apikey;
    if( !checkAlnum(apikey) )
        throw 'apikey invalid';

    var conf = await readConfigFile(apikey);

    if( event.path == '/todoist-callback' ){
        var param = {
            client_id: TODOIST_CLIENT_ID,
            client_secret: TODOIST_CLIENT_SECRET,
            code: body.code
        };
        var json = await do_post("https://todoist.com/oauth/access_token", param );

        conf.token = json;
        await writeConfigFile(apikey, conf);

        return new Response({});
        }else

body.codeが、ブラウザから取得した認可コードです。
そうすると、todoistサーバから、APIトークンが返ってくるので、それをユーザ識別子ごとのファイルに保存します。ユーザ識別子はブラウザ側からAPI KeyとしてHTTPヘッダに指定してもらいます。

あとは、ブラウザからのリクエストに対して、todoistのnpmモジュールTodoistを使ってリストを取得します。取得には、APIトークンが必要ですので、ユーザ識別子ごとのファイルから取り出して使っています。

api/contollers/todoist/index.js
    if( event.path == '/todoist-list' ){
        const todoist = Todoist(conf.token.access_token);

        await todoist.sync();
        const projects = todoist.projects.get();
        var favorite = projects.filter(item => item.is_favorite );
        var favorite_ids = favorite.map(item => item.id);
        const items = todoist.items.get();
        var favorite_items = items.filter(item => favorite_ids.includes(item.project_id));

        const notes = todoist.notes.get();
        var item_ids = favorite_items.map(item => item.id);
        var favorite_notes = notes.filter(item => item_ids.includes(item.item_id));

        return new Response({items: favorite_items, projects: projects, notes: favorite_notes });
    }

あと、細かな処理をしていますが、やっているのは、
・プロジェクトIDからプロジェクト名に変換するために、プロジェクト一覧を取得
・プロジェクトのリストから、お気に入りにしたプロジェクトのIDを取得
・すべてのリストを取得し、お気に入りのプロジェクトIDのものを抽出
・すべてのノートを取得し、お気に入りのプロジェクトIDのものを抽出

さきほどの、ブラウザ側とサーバ側の間の認可コードのやり取りの部分は以下です。

public/js/start.js
        if( searchs.code ){
            var param = {
                code: searchs.code
            };
            history.replaceState(null, null, '.');
            do_post_apikey(base_url + '/todoist-callback', param, searchs.state)
            .then(json =>{
                console.log(json);
                Cookies.set("todo_apikey", searchs.state, { expires: EXPIRES });
                this.apikey = searchs.state;
                this.todo_list_update()
                .then(() =>{
                    setInterval( () =>{
                        this.todo_list_update(true);
                    }, UPDARTE_INTERVAL * 60 * 1000);
                });
            });

認証およびAPIキーがサーバ側で保持出来たら認証完了です。ブラウザ側では、ユーザ識別子をapikeyとしてCookieに保持しておきます。

リスト取得は以下の部分です。

public/js/start.js
        todo_list_update: async function(silent){
            if( !this.apikey )
                return;

            try{
                if( !silent )
                    this.progress_open();
                var param = {};
                var json = await do_post_apikey(base_url + '/todoist-list', param, this.apikey);

                this.todo_projects = json.projects;
                this.todo_notes = json.notes;

                var today = new Date();
                today.setHours(0, 0, 0, 0)
                var todayTime = today.getTime();
                var tomorrow = new Date();
                tomorrow.setHours(0, 0, 0, 0)
                tomorrow.setDate(tomorrow.getDate() + 1);
                var tomorrowTime = tomorrow.getTime();

                this.todo_list_expire = json.items.filter( item => item.due && Date.parse(item.due.date) < todayTime );
                this.todo_list_today = json.items.filter( item => item.due && Date.parse(item.due.date) >= todayTime && Date.parse(item.due.date) < tomorrowTime );
                this.todo_list_other = json.items.filter( item => item.due && Date.parse(item.due.date) >= tomorrowTime );
                this.todo_list_someday = json.items.filter( item => !item.due );
            }catch(error){
                console.error(error);
                alert(error);
            }finally{
                if( !silent )
                    this.progress_close();
            }
        },

これまたいろいろやっていますが、要は、期限切れのタスクの抽出、今日のタスクの抽出、明日以降のタスクの抽出、期限が設定されていないタスクの抽出、をしています。
this.todo_list_XXXXという変数に格納していますが、あとはVueが表示をよろしくやってくれます。

ブラウザで表示してみましょう。

 https://【立ち上げたサーバのURL】/index.html

最初に、API Keyの設定が必要です。「API Key」ボタンを押下して、ユーザ識別子を設定します。

image.png

さきほど追加した「洗濯」が表示されています。
期限を設定していなかったので、いつか のグループに入っています。

image.png

todoistのAndroidアプリでもいいですし、todoistのWebページからでもどちらでよいですが、「洗濯」タスクの期限を今日にしてみましょう

image.png

10分ごとに、リロードしておいたので、ちょっと?待てば、今日のタスクに移動するかと思います。

PWA化する

詳細は、以下を参考にしてください。今回はPush通知は使っていません。

PWAを試してみよう

以下を追記しています。

index.htmlに以下を追加。

public/index.html
  <link rel="manifest" href="manifest.json">

ページロード直後に以下を呼び出し。

public/js/start.js
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('sw.js').then(async (registration) => {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch((err) => {
                console.log('ServiceWorker registration failed: ', err);
            });
        }

sw.jsを作成し、public/sw.jsにデプロイ

これで、右上のアドレスバーのところの⊕を押せば、PWAアプリとしてインストールできるようになっているかと思います。

image.png

Androidでアプリ固定化

余っているAndroidタブレットを使って、常時リスト表示したいと思います。

Androidから同様にブラウザ(Chrome)から開き、メニューから「アプリをインストール」を選択して、アプリとしてインストールしておきます。
まずは、単独で起動でき、リストが表示されるところまで確認しておきます。

次に、アプリを固定化します。
(以降は、Androidのバージョンによって見え方は違うかもしれません。)

image.png

Androidの「設定」→「セキュリティ」→「アプリの固定」を選択します。
おそらくOFFになっているかと思いますので、ONにします。

次に、現在実行中のアプリ一覧を表示し、PWAアプリとして起動したものの上にあるアイコンをタッチします。
そうすると、「固定」があるので、それをタッチします。

image.png

こんな表示が出てきますので、よく読んで「OK」をタッチします。
これで、常時表示ができました。他のアプリが選択できなくなったと思います!

image.png

できました!

image.png

アプリ固定を解除して戻りたい時には、ホームボタンと戻るボタンを一緒に長押しすれば戻れます。

以上

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

AlexaとTodoistでやることリスト・お買い物リスト

Todoistって、Amazon Echoと連携できるんですね!
しかもWebAPIが充実しているので、さらに連携の輪が広がりそうです。

ちなみに、Todoistは、タスク管理ツールです。一般には、やることリストとか、買い物リストが挙げられますが、メモ的に使えて、かつ、期限を設定して予定を立てたり、失念するのを避けるのに役立ちます。

todoist
 https://todoist.com/

今回作成する全体の構成はこんな感じです。

image.png

すでに、todoistはAlexaとの連携をサポートしていますし、AndroidやiPhone用のアプリもありますので、その部分は特に難しいところはありません。
今回は、やることリスト・買い物リストの表示をWebページとして表示します。todoistはオフィシャルでWebAPIが充実していますし、npmモジュールもあるので、活用の幅が広がります。
また、さらに、PWA化することでネイティブアプリ化し、それをAndroidで固定アプリに設定することで、やることリスト表示の専用機に仕立て上げます。

毎度のことですが、ソースコードもろもろをGitHubに上げておきました。

poruruba/todolist
 https://github.com/poruruba/todolist

準備

todoistのアカウント登録

まずは、以下のURLから、todoistのアカウント登録をします。

todoist
 https://todoist.com/ja

image.png

「はじめる」ボタンまたは右上のサインアップをクリックします。

image.png

ここでは、スマートフォンにインストールしているAmazon Alexaアプリに設定しているアカウントでサインアップし、認証を完了させます。

image.png

これでアカウントが登録されました。

todoistアプリをインストール

Google Playから、Todoistをインストールします。

image.png

todoistにアカウント登録したときのアカウントでログインします。

image.png

ログインが完了しました。

image.png

Alexa連携を設定

すでに、スマートフォンに、「Amazon Alexa」がインストールされている前提で進めます。
以降は、Androidでの操作です。

まずは、下の方の「その他」→「設定」→「リスト」を選択します。

image.png

そうすると、Any.do、AnyList、Todoistが表示されていますので、Todoistの右側の⊕をタッチします。

image.png

以下のように表示されるので、「有効にして使用する」ボタンを押下します。

image.png

リストへのアクセス権の確認が表示されますので、「アクセス権を保存」ボタンを押下します。

image.png

そうすると、todoistのサイトに飛んで、Agreeするかが聞かれますので、「Agree」ボタンを押下します。

image.png

これで、連携設定が完了しました。

image.png

もう一度、todoistアプリの方を開いてみましょう。
そうすると、左上の三のマークをタッチし、プロジェクトのところをタッチすると、「Alexa ToDo リスト」というのと、「Alexaの買い物リスト」が増えているのがわかります。

image.png

最後に、この「Alexa ToDo リスト」と「Alexaの買い物リスト」をお気に入りにしておきます。
「プロジェクト」→「プロジェクトを管理」をタッチし、「Alexa ToDo リスト」と「Alexaの買い物リスト」の右側にあるハートマークをタップし、赤色にします。これによって、この2つがお気に入りのプロジェクトとなりました。この意味はあとで、わかります。

image.png

あと、お好みで、todoistアプリをAndroidのホーム画面にウィジェットとして登録してもよいかと思います。

とりあえずAlexa+todoist連携を試してみる

以下に、発話例がありますので、試してみましょう。

Amazon Alexa で Todoist を使う
 https://get.todoist.help/hc/ja/articles/360010721059-Amazon-Alexa-%E3%81%A7-Todoist-%E3%82%92%E4%BD%BF%E3%81%86

例えば以下をAmazon Echoに話しかけてみましょう。

・アレクサ、やることリストに洗濯を追加して。
・アレクサ、今日のやることリストは?
・アレクサ、やることリストの洗濯を完了にして
・アレクサ、買い物リストに納豆を追加して
・アレクサ、今日の買い物リストは?
・アレクサ、買い物リストの納豆を完了にして

めでたく、こんな感じで追加されました。

image.png

Node.jsサーバからtodoistのリストを取得する

たいていの方は上記まででよいのですが、勉強をかねて、拡張に挑戦します。
Node.jsからの操作には、以下のnpmモジュールを利用させていただきました。

romgrk/node-todoist
 https://github.com/romgrk/node-todoist

以下も使っています。

node-fetch/node-fetch
 https://github.com/node-fetch/node-fetch

リスト取得する前に、ユーザごとにAPIトークンの取得が必要です。

todoist Developer: Authorization
 https://developer.todoist.com/sync/v8/#authorization

OAuthに似たやり取りで、ブラウザ側とサーバ側での連携が必要です。

まずは、サーバのURLをtodoistに登録しておく必要があります。以下のURLを開いて、「Create a new app」ボタンを押下します。

App Management Console
 https://developer.todoist.com/appconsole.html

image.png

App display nameには適当な名前を、App service URLには、これから立ち上げるNode.jsのWebページのURLを指定しておきます。

次に作成したappを選択肢、OAuth redirect URLを指定します。後で作成するのですが、立ち上げるNode.jsのWebページと同じにしておきます。(Single Page Applicationとして実装するため)。最後に、「Save settings」ボタンを押下します。

image.png

その時に表示される、「Client ID」「Client secret」を覚えておきます。後で使うので。

①サーバ側に、ユーザ識別子.jsonというファイルを作成しておきます。
これがばれてしまうと、ログインされてしまいますので、ユーザ識別子は、推測されにくいランダムな値にしましょう。

②ブラウザからClient IDを指定して、ログインを開始する。

以下のように形成されるURLにジャンプします。

https://todoist.com/oauth/authorize?client_id=" + TODOIST_CLIENT_ID + "&scope=data:read&state=" + value

TODOIST_CLIENT_IDは、先ほど覚えておいたClient IDです。stateには正しくは乱数等推測されにくいものにするのですが、手を抜いていて、ユーザの識別子を指定します。①の通り、このユーザの識別子の名前+.jsonで、サーバ側にファイルが作成されている前提です。

③todoistのログインページが表示されるので、ログインする。

todoistのアカウント登録時に使った認証アカウントでログインします。

④認可コードを取得する

ログインが完了すると、OAuth redirect URLで指定したURLにジャンプしてきます。その時に、認可コードとstateが返ってきます。
stateは、②で指定した値のはずです。

⑤認可コードからAPIトークンを取得する

認可コードを取得したので、これを使って以降のtodoistのWebAPI呼び出しに必要なAPIトークンを取得します。子の取得には、Client secretが必要です。秘匿の値として扱う必要があるので、Nodeサーバに渡して、サーバ側で実施します。

api/controllers/todoist/index.js
exports.handler = async (event, context, callback) => {
    var body = JSON.parse(event.body);
    var apikey = event.requestContext.apikeyAuth.apikey;
    if( !checkAlnum(apikey) )
        throw 'apikey invalid';

    var conf = await readConfigFile(apikey);

    if( event.path == '/todoist-callback' ){
        var param = {
            client_id: TODOIST_CLIENT_ID,
            client_secret: TODOIST_CLIENT_SECRET,
            code: body.code
        };
        var json = await do_post("https://todoist.com/oauth/access_token", param );

        conf.token = json;
        await writeConfigFile(apikey, conf);

        return new Response({});
        }else

body.codeが、ブラウザから取得した認可コードです。
そうすると、todoistサーバから、APIトークンが返ってくるので、それをユーザ識別子ごとのファイルに保存します。ユーザ識別子はブラウザ側からAPI KeyとしてHTTPヘッダに指定してもらいます。

あとは、ブラウザからのリクエストに対して、todoistのnpmモジュールTodoistを使ってリストを取得します。取得には、APIトークンが必要ですので、ユーザ識別子ごとのファイルから取り出して使っています。

api/contollers/todoist/index.js
    if( event.path == '/todoist-list' ){
        const todoist = Todoist(conf.token.access_token);

        await todoist.sync();
        const projects = todoist.projects.get();
        var favorite = projects.filter(item => item.is_favorite );
        var favorite_ids = favorite.map(item => item.id);
        const items = todoist.items.get();
        var favorite_items = items.filter(item => favorite_ids.includes(item.project_id));

        const notes = todoist.notes.get();
        var item_ids = favorite_items.map(item => item.id);
        var favorite_notes = notes.filter(item => item_ids.includes(item.item_id));

        return new Response({items: favorite_items, projects: projects, notes: favorite_notes });
    }

あと、細かな処理をしていますが、やっているのは、
・プロジェクトIDからプロジェクト名に変換するために、プロジェクト一覧を取得
・プロジェクトのリストから、お気に入りにしたプロジェクトのIDを取得
・すべてのリストを取得し、お気に入りのプロジェクトIDのものを抽出
・すべてのノートを取得し、お気に入りのプロジェクトIDのものを抽出

(参考) todoist Sync API
 https://developer.todoist.com/sync/v8/

さきほどの、ブラウザ側とサーバ側の間の認可コードのやり取りの部分は以下です。

public/js/start.js
        if( searchs.code ){
            var param = {
                code: searchs.code
            };
            history.replaceState(null, null, '.');
            do_post_apikey(base_url + '/todoist-callback', param, searchs.state)
            .then(json =>{
                console.log(json);
                Cookies.set("todo_apikey", searchs.state, { expires: EXPIRES });
                this.apikey = searchs.state;
                this.todo_list_update()
                .then(() =>{
                    setInterval( () =>{
                        this.todo_list_update(true);
                    }, UPDARTE_INTERVAL * 60 * 1000);
                });
            });

認証およびAPIキーがサーバ側で保持出来たら認証完了です。ブラウザ側では、ユーザ識別子をapikeyとしてCookieに保持しておきます。

リスト取得は以下の部分です。

public/js/start.js
        todo_list_update: async function(silent){
            if( !this.apikey )
                return;

            try{
                if( !silent )
                    this.progress_open();
                var param = {};
                var json = await do_post_apikey(base_url + '/todoist-list', param, this.apikey);

                this.todo_projects = json.projects;
                this.todo_notes = json.notes;

                var today = new Date();
                today.setHours(0, 0, 0, 0)
                var todayTime = today.getTime();
                var tomorrow = new Date();
                tomorrow.setHours(0, 0, 0, 0)
                tomorrow.setDate(tomorrow.getDate() + 1);
                var tomorrowTime = tomorrow.getTime();

                this.todo_list_expire = json.items.filter( item => item.due && Date.parse(item.due.date) < todayTime );
                this.todo_list_today = json.items.filter( item => item.due && Date.parse(item.due.date) >= todayTime && Date.parse(item.due.date) < tomorrowTime );
                this.todo_list_other = json.items.filter( item => item.due && Date.parse(item.due.date) >= tomorrowTime );
                this.todo_list_someday = json.items.filter( item => !item.due );
            }catch(error){
                console.error(error);
                alert(error);
            }finally{
                if( !silent )
                    this.progress_close();
            }
        },

これまたいろいろやっていますが、要は、期限切れのタスクの抽出、今日のタスクの抽出、明日以降のタスクの抽出、期限が設定されていないタスクの抽出、をしています。
this.todo_list_XXXXという変数に格納していますが、あとはVueが表示をよろしくやってくれます。

ブラウザで表示してみましょう。

 https://【立ち上げたサーバのURL】/index.html

最初に、API Keyの設定が必要です。「API Key」ボタンを押下して、ユーザ識別子を設定します。

image.png

さきほど追加した「洗濯」が表示されています。
期限を設定していなかったので、いつか のグループに入っています。

image.png

todoistのAndroidアプリでもいいですし、todoistのWebページからでもどちらでよいですが、「洗濯」タスクの期限を今日にしてみましょう

image.png

10分ごとに、リロードしておいたので、ちょっと?待てば、今日のタスクに移動するかと思います。

PWA化する

詳細は、以下を参考にしてください。今回はPush通知は使っていません。

PWAを試してみよう

以下を追記しています。

index.htmlに以下を追加。

public/index.html
  <link rel="manifest" href="manifest.json">

ページロード直後に以下を呼び出し。

public/js/start.js
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('sw.js').then(async (registration) => {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch((err) => {
                console.log('ServiceWorker registration failed: ', err);
            });
        }

sw.jsを作成し、public/sw.jsにデプロイ

これで、右上のアドレスバーのところの⊕を押せば、PWAアプリとしてインストールできるようになっているかと思います。

image.png

Androidでアプリ固定化

余っているAndroidタブレットを使って、常時リスト表示したいと思います。

Androidから同様にブラウザ(Chrome)から開き、メニューから「アプリをインストール」を選択して、アプリとしてインストールしておきます。
まずは、単独で起動でき、リストが表示されるところまで確認しておきます。

次に、アプリを固定化します。
(以降は、Androidのバージョンによって見え方は違うかもしれません。)

image.png

Androidの「設定」→「セキュリティ」→「アプリの固定」を選択します。
おそらくOFFになっているかと思いますので、ONにします。

次に、現在実行中のアプリ一覧を表示し、PWAアプリとして起動したものの上にあるアイコンをタッチします。
そうすると、「固定」があるので、それをタッチします。

image.png

こんな表示が出てきますので、よく読んで「OK」をタッチします。
これで、常時表示ができました。他のアプリが選択できなくなったと思います!

image.png

できました!

image.png

アプリ固定を解除して戻りたい時には、ホームボタンと戻るボタンを一緒に長押しすれば戻れます。

以上

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

turfjs の import 戦略について考える

turf.js を使うには、

  • 全てのモジュールが含まれた @turf/turf を import する
  • 個々の機能の @turf/xxxx を複数選んで import する

の方法があります。

全てのモジュールが含まれた @turf/turf を import する方法

npm i --save @turf/turf
// index.js
import { buffer, lineString, LineString, Feature } from '@turf/turf';

const feature: Feature<LineString> = lineString([[130, 34],[131, 35]]);
const buffered = buffer(feature, 5, { units: 'meters' });

個々の機能の @turf/xxxx を複数選んで import する方法

npm i --save @turf/helpers @turf/buffer
// index.js
import { lineString, LineString, Feature } from '@turf/helpers';
import buffer from '@turf/buffer';

const feature: Feature<LineString> = lineString([[130, 34],[131, 35]]);
const buffered = buffer(feature, 5, { units: 'meters' });

個々の機能を選んで import する場合、基本となるデータ型やそれに係わる関数は @turf/helpers に入っているので、実質 @turf/helpers は必須になるでしょう。
この例では Buffer 関数を使うため、 @turf/buffer をインストールし、import します。
@turf/turf を使う場合とでは import 文が変わるだけで、実装コードは変わりません。

@turf/turf の import では接頭辞を付けられるのがよいのだけど…

私はこれまで @turf/turf を好んで使っていました。
しかも、import で * as turf とすることで、すべての型や関数を turf.xxxx と書くようにしていました。

// index.js
import * as turf from '@turf/turf';

const feature: turf.Feature<turf.LineString> = turf.lineString([[130, 34],[131, 35]]);
const buffered = turf.buffer(feature, 5, { units: 'meters' });

どこで turf.js を使っているかが分かりやすいためで、他のライブラリでもこのような書き方をする場合もあります。
特に Feature, Geometry などの GeoJSON 由来の型は、併用することが多い Mapbox GL JS のライブラリ にも存在しており、よく混同します。
なので、turfjs 用の Feature は turf.Feature と明示的に記述して差別化しています。

機能を個々に選択する場合では、以下のように別名をそれぞれ付与することができますが、、、。

// index.js
import { 
  lineString as turf_lineString, 
  LineString as turf_LineString, 
  Feature as turf_Feature
} from '@turf/helpers';
import { default as turf_buffer } from '@turf/buffer';

const feature: turf_Feature<turf_LineString> = turf_lineString([[130, 34],[131, 35]]);
const buffered = turf_buffer(feature, 5, { units: 'meters' });

指定方法がめんどいのと、できれば turf_ ではなく、 turf. と書きたいところです。

んで一括と個別、どちらがよいの?

一括でダウンロードされる? turf.min.js はファイルサイズが 550KB 程度あり小さくはありません。
しかし、webpack などのモジュールバンドラーを使うなら、使わない関数などを刈り取って必要な機能だけに圧縮できそうです。

私の場合、Angular や node.js の Web Server で使うことが多いため、Webpack での shrink に期待して、一括版の @turf/turf を使っておけばよいかなあ、という判断を現在はしています。

「個別版は○○という用途で使うのだ」という知見があれば教えてください。

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

【React Hooks】非同期処理をいい感じに扱うuseAsyncを自分でつくる。

始めに

3 amazing REACT HOOKS to keep your code organized neatly』この記事を読んでいてうち一つは結構有益だなぁぁって思ったので紹介です!本編中のコードはここからの引用をちょっと修正したものになります。
react-useをご存知の方も改めてこうやって作れるなってコードベースが参考になったらなと思っています。

  • あるある非同期処理
  • useAsyncを使った場合
  • useAsyncの作りかた
  • react-use

といった形で紹介していきます。

As is:例えばあるあるの非同期処理

function MyComponent() {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  const [result, setResult] = useState(null)

  useEffect(() => {
    (async () => {
        setLoading(true)
        try {
            const result = await doSomeAction();
            setResult(result);
        } catch (e) {
            setError(e.toString());
        } finally {
            setLoading(false);
        }
    })();
  }, [])

  if (loading) {
    return <>loading...</>
  }

  if (error) {
  return <>{error}</>
  }

  return <>{result}</>
}

といった形の処理です。
doSomeAction()という何らかの処理によって、非同期で動作します。そして、それは何らかの結果やエラーとして帰ってきます。
そこでこのようにuseStateを絡めながらコードを書いていくことがしばしばあるかなと思います。
とはいえ、これを何度か書いていくのは煩雑感があります・・・。そこで、react-apollouseQuery みたいな形でかけたら幸せじゃないですか?

To be:useAsyncのある世界

function MyTidyComponent() {
    const {loading, result, error} = useAsync(doSomeAction)

    if (loading) {
      return <>loading...</>
    }

    if (error) {
      return <>something broke</>
    }

    return <>{result}</>
}

随分とシンプルになりました。
ではこれを作っていきましょう!というのが今日の本題。

useAsyncのつくりかた

function useAsync(asyncFunction, immediate = true) {
  const [loading, setLoading] = useState(false)
  const [result, setResult] = useState(null)
  const [error, setError] = useState(null)

  const mounted = useRef(true)

  useEffect(() => {
    return () => {
      mounted.current = false
    }
  }, []);

  const execute = useCallback(async (...args) => {
    setLoading(true)
    setResult(null)
    setError(null)
    try {
      const r = await asyncFunction(...args)
      if (mounted.current) {
        setResult(r);
      }
      return r
    } catch (e) {
      if (mounted.current) {
        setError(e);
      }
    } finally {
      if (mounted.current) {
        setLoading(false);
      }
    }
  }, [asyncFunction])

  useEffect(() => {
    if (immediate) {
      execute()
    }
  }, [execute, immediate]);

  return { execute, loading, result, error };
}

ほとんどはAs is のコードをそのまま持ってきたのかのような形です。
immediateはおしゃれポイントみたいですね。コンポーネントがマウントされてもimmediateをfalseにしておけば実行されず、trueに変更した時に実行されます。

  useEffect(() => {
    if (immediate) {
      execute()
    }
  }, [execute, immediate]);

useRefの記述や条件分岐はメモリリーク対策でこれが一度実行されてアンマウントされた場合に以下のコードが動かないようにしています。

  useEffect(() => {
    return () => {
      mounted.current = false
    }
  }, []);

クリーンアップ関数でcurrentをfalseにしていますね。
といった形なんですがreac-useではこれよりももうちょっとだけ内部的に良いuseAsyncが提供されています。
じゃあなんで書き方を紹介したの?ってなるんですが、これは内部実装についてしるのが面白かったのと、簡単にかけるのでimmediatelyのようなカスタマイズを施せるので知っておいて損はないなと思ったからです。

react-use

react-use

import {useAsync} from 'react-use';

const Demo = ({url}) => {
  const state = useAsync(async () => {
    const response = await fetch(url);
    const result = await response.text();
    return result
  }, [url]);

  return (
    <div>
      {state.loading
        ? <div>Loading...</div>
        : state.error
          ? <div>Error: {state.error.message}</div>
          : <div>Value: {state.value}</div>
      }
    </div>
  );
};

おしゃれポイントはなくなっていますが、ほとんどおんなじです。どこら辺が少し高性能なのかでいくと、第二引数がdepsになっておりdepsが変更された時、非同期関数が作り直されます。また、mounted.currentのような判定に加えて呼び出し回数による制御もかかっています。
他にはuseIntervalやuseTimeoutなどの使い勝手もよいものがたくさん提供されているのでつかっていくのが良さそうですね。

まとめ

  • 基本的にはreact-useを使った方が楽
  • だけどカスタマイズを重ねていきたい時はこんな感じでuseAsyncみたいなものをつくることができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-konvaライブラリが使って便利だったので使用法などをメモ

概要

HTML5の世界では<canvas>というタグが存在し、2Dグラフィックスを描くことができます。
ただ、素の<canvas>は操作処理が手続き型なので、宣言的に記述できるライブラリが欲しいと思っていました。

そこで目をつけたのがKonva Framework。こちらは2Dグラフィックスを描くためのJavaScriptライブラリなのですが、それをReact風に宣言的に使えるreact-konvaというライブラリもあります。なので今回は、react-konvaのざっくりとした使用感について解説します。

導入

# npm の場合
npm install react-konva konva --save
# yarnの場合
yarn add react-konva konva

概念

このライブラリで宣言的に記述できるのは、<Stage>タグ、<Layer>タグ、<Group>タグ、<Rect><Text><Image>などの各種描画用タグです。階層構造としては次の通り。

              Stage
                |
         +------+------+
         |             |
       Layer         Layer
         |             |
   +-----+-----+     Shape
   |           |
 Group       Group
   |           |
   +       +---+---+
   |       |       |
Shape   Group    Shape
           |
           +
           |
         Shape

(出典:https://konvajs.org/docs/overview.html)

  • <Stage>タグ……描画の土台となるタグ。おおよそ<canvas>タグに相当。<canvas>の表示サイズや表示スケールを決めたり、これより内側の描画をtoDataUrl()メソッドで画像化したりするのに使える
  • <Layer>タグ……名前の通り、描画レイヤーとして機能するタグ。z-index、つまり表示順をこの単位で管理できる
  • <Group>タグ……こちらは必須ではないが、描画用タグをグルーピングするために使う。グループ単位で描画を拡大・縮小・回転・移動させることができる
  • 描画用タグ……四角形を描画する<Rect>、テキストを描画する<Text>、画像を描画する<Image>などのタグ。塗りつぶしの方法や透明度など、細かな設定も可能

より詳しい概要については、Konva自体の概要説明ページを読むと良いでしょう。

基本的な使い方

まずは、500x500ピクセルの<canvas>内に、左上座標が(100, 100)で、大きさが300x200な赤色の長方形を描いてみます。
見やすさの問題から、その長方形よりも先に、外枠として辺だけの長方形も黒線で描画しておきます。
ちなみに、いずれの描画用タグからも塗りつぶされていない箇所は透明色になっています。

import { Layer, Rect, Stage } from "react-konva";
// (中略)
return <Stage width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
  </Layer>
</Stage>;

image.png

真円を描く場合は<Circle>、楕円を描く場合は<Ellipse>、(折れ曲がりも含めた)直線を引く場合は<Line>を使用します。
他にも様々な描画用タグはありますが、使い方はKonvaのドキュメントを読めば察せられるかと。
後、opacity属性は透明度を決めるもので、0が完全透明・1が無透明です。

<Stage width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
    <Circle fill='blue' x={100} y={100} radius={50} opacity={0.5} />
    <Ellipse fill='green' x={250} y={300} radiusX={100} radiusY={180} />
    <Line points={[400, 50, 300, 150, 150, 170, 300, 50]} stroke='purple' strokeWidth={15} />
  </Layer>
</Stage>

image.png

面白いのが<Text>タグ。純正<canvas>におけるテキスト描画用関数(fillTextメソッドやstrokeTextメソッド)では自動で折り返してくれないのに対し、<Text>タグでは(最大横幅を設定すると)自動で折り返してくれます。また、テキスト内に改行を入れると、それに従って改行を入れてくれます。

<Stage width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Text text={text} x={50} y={50} width={400} fontSize={40} fontFamily={'Calibri'} fill='black' align='left' />
  </Layer>
</Stage>

image.png

画像読み込みも可能です。サンプル画像は貼りませんが、指定したサイズにリサイズした上で貼ってくれます。
また、image属性に渡せるものは、Canvas APIにおける画像描画で使用できるそれと同様です。つまり、TypeScriptで言えば、

  • HTMLImageElement型……<img>タグ自体のDOM
  • SVGImageElement型……<svg>タグ自体のDOM
  • HTMLVideoElement型……<video>タグ自体のDOM
  • HTMLCanvasElement型……<canvas>タグ自体のDOM
  • ImageBitmap型……事前に描画のための処理を済ませた画像データ。Serializableなので保存しやすい
  • OffscreenCanvas型……名前の通り、画面に描画することを考慮してない<canvas>。データキャッシュ用に利用できる

が利用可能です。

<Stage width={500} height={500}>
  <Layer>
    <Image image={imageData} x={50} y={50} width={300} height={300} />
  </Layer>
</Stage>

座標系について

<Stage>タグに設定するwidth属性やheight属性はピクセル単位ですが、実は、<Layer>タグ以下に配置した各種描画タグで使うx属性やy属性などで設定するピクセル単位と微妙に異なります。以下では、両者をそれぞれ、次のように言い分けます。

  • 見かけ上のピクセル……<Stage>タグに設定するwidth属性やheight属性に設定するもの
  • 描画上のピクセル……各種描画タグで使うに設定するxywidthheight属性などに設定するもの

見かけ上のピクセルは<canvas>自体の表示サイズに関わります。一方、描画上のピクセルは、<Stage>タグに別途設定したscale属性の値に従い、実際に描画される際のサイズが変わります。例えば、次のように書いたとしましょう。

const scaleX = 0.5;
const scaleY = 0.4;
return <Stage scale={{x: scaleX, y: scaleY}} width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
  </Layer>
</Stage>;

ここで、<Rect>がタグどのように描画されるかを考えます。<Stage>タグにscale属性を設定した影響で、x属性やwidth属性はscaleX倍された大きさとして描画されます(y属性などはscaleY倍)。
しかし、<canvas>自体は<Stage>タグのwidth属性・height属性の値に従うので、結局、「500x500ピクセルの画像の中に、左上座標(50, 40)から大きさ150x80の赤い長方形が描画」されます。

image.png

<Stage>自体の大きさもscaleに合わせたいなーって時は、<Stage>自体のwidthheight属性もスケーリングする必要があります。ただし、その場合でも、描画上のピクセルに対応する、各種描画タグに、いちいちscaleXだのscaleYだのを使う必要はありません。

const scaleX = 0.5;
const scaleY = 0.4;
return <Stage scale={{x: scaleX, y: scaleY}} width={500 * scaleX} height={500 * scaleY}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
  </Layer>
</Stage>;

image.png

画像保存について

<canvas>で描画したものを画像データ化したいことはよくあると思います。<Stage>においてもそういった場合の処理は提供されていますが、React的にはuseRefを使うのがベターでしょう。具体的にはこんな感じ。

import React, { useRef } from "react";
import { Stage as StageType } from 'konva/types/Stage';

const stageRef = useRef<StageType>(null);

const process = () => {
  const temp = stageRef.current;
  // stageRefの中身(temp )がnullな可能性を考慮してチェック
  if (temp !== null) {
    // dataUrlに、画像データがdata URL(MIME Type + base64文字列)形式で書き込まれる。
    // toDataURLの引数を変更すれば、PNG以外の画像形式への変換も可能
    const dataUrl = temp.toDataURL();
    // これ以降、dataUrlを使った操作(画像保存、画像加工等)を行う
  }
};

return <Stage ref={stageRef} width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
  </Layer>
</Stage>;

興味深いのは、toDataURLメソッド内の引数にpixelRatioというのがあることです。例えばtemp.toDataURL({pixelRatio: 2})とすれば、<Stage>width100height200だった場合、200x400ピクセルの画像として書き出されます。
つまり、例えば、「<canvas>をWebページ上では小さく表示するが、ダウンロードする際は大きい状態にする」ことが簡単にできます。

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

async await Promiseがよく分からない

想定読者

Javascriptは触ったことがあるけれどasync await Promiseがなんなのかよくわからない。
なんか聞いたことはあって調べたことがあるけど良く分からなかった。

必要な知識

簡単なJavascriptの構文(ES6)
匿名関数(無名関数)

私も2年くらい前からJSは触っているのですがasync await Promiseがなんなのか全く分かっていませんでした。
最近業務でサーバーレスアプリケーション、バックエンドにfirebase、フロントにJSという構成でアプリケーションを作った時に頻繁にAsync Await Promiseが出てきたので調べました。

調べる前は私もなんだか難しそうだと抵抗があったのですが分かってくるとそこまで難しいことはないと思えるようになりました。
なんかドキュメントの用語は難しいこと書いてあってよく分からないという人に向けて少しで分かりやすく書ければと思っています。

記事内に出現するコードは基本的にGoogle chromeのディベロッパーコンソールで動くので動作を確認しながら進めるとなお良いかと思います。

Promiseとは

Promiseとはオブジェクトです。Promiseオブジェクトは下記のようなコードで取得できます。

1.Promiseのインスタンスを生成したとき

pboj = new Promise((resolve) => {resolve("aaaa")})
->Promise{<fulfilled>: "aaaa"}

2.async functionの返り値

pboj = (async() => {return("aaaa")})()
->Promise{<fulfilled>: "aaaa"}

3.Promiseオブジェクトを返すメソッド(例.fetch)

pobj = fetch('https://example.com')

上記の例では全て変数pobjの中にPromiseオブジェクトが格納されたことになります。
resolveやasyncについても順番に説明していきますがここではまずPromiseというのはオブジェクトであるということを押さえていただければと思います。

asyncとは

asyncとは返り値にPromiseオブジェクトを返す関数です。

result = (() => {return("aaaa")})()
->"aaaa"

pobj = (async() => {return("aaaa")})()
->Promise{<fulfilled>: "aaaa"}

asyncがついていないfunctionであれば返り値は"aaaa"という文字列になりますが
asyncがついているので返り値はPromiseオブジェクトになります。

await

awaitはasync functionの中でしか使えません。
そしてawaitはPromiseオブジェクト内で行われている処理が完了するまで、async function内の処理をその場で一時停止するという特徴があります。

resolve,reject

pobj = (async() => {
  return new Promise((resolve,reject) => {
    setTimeout(() => {
      try{
        resolve('aaaa')
      }catch{
        reject('bbbb')
      }
    }, 2000)
  })
})()

console.log(pobj)
console.log(await pobj)
console.log(pobj)

//結果
//Promise {<pending>}
//aaaa
//Promise{<fulfilled>: "aaaa"}

ここでPromise<pending>Promise<fulfilled>というのが出てきたと思います。
pendingとは文字通り処理が保留中で完了していないことを示します。
fulfilledはPromiseオブジェクトが解決したことを意味します。
エラーを発生させた場合はfulfilledの部分がrejectedになります。

pobj = (async() => {
  return new Promise((resolve,reject) => {
    setTimeout(() => {
      try{
        fdas
        resolve('aaaa')
      }catch{
        reject('bbbb')
      }
    }, 2000)
  })
})()

console.log(pobj)
await pobj.catch((reject) => console.log(reject))
console.log(pobj)

//結果
//Promise {<pending>}
//bbbb
//Promise{<rejected>: "bbbb"}

pending以外の状態はsettledと呼ばれます。
つまりawaitというのはPromiseオブジェクトがsettledになることを担保するということが分かります。

※async function内のreturnはresolveを意味します。

pobj = (async() => {return 'aaaa'})()
Promise{<fulfilled>: "aaaa"}

どのようなケースで使うか

実際どのようなケースで上記で説明したasync await promiseが使われるのでしょうか
まずは下記のプログラムをみてください。

console.log('start')

let bool = true
pobj = (async() => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(bool ? '' : '')
    }, 2000)
  })
})()

プログラム自体は単純です。変数boolの中がtrueなら'真'をfalseなら'偽'という文字列を出力するものです。
setTimeoutを使って2秒後にPromiseが解決するようにしていますが実際に現場ではここでは長く時間がかかる処理(例えばDBからデータを取得するなど)が来ます。

console.log('start')

let bool = true
pobj = (async() => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(bool ? '' : '')
    }, 2000)
  })
})()

exec = (async() => {
  result = await pobj
  //awaitよりの下の処理はpobj,resultは必ずsettledである
  console.log(pobj)
  console.log(result)
  console.log('finish')
})()

//結果
//start
//Promise {<pending>}
//Promise {<fulfilled>: "真"}
//真
//finish

結果をみていただくと問題なく動いているのが分かると思います。
念のためawaitがなかったらどうなるでしょうか。
下記の結果をご覧ください。

console.log('start')

let bool = true
pobj = (async() => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(bool ? '' : '')
    }, 2000)
  })
})()

exec = (async() => {
  result = pobj
  console.log(pobj)
  console.log(result)
  console.log('finish')
})()

//結果
//start
//Promise{<pending>}
//Promise{<pending>}
//finish

上記のような状態になります。
result = pobjにawaitがないためpobjやresultの状態がsettledであることが担保されません。
結果Promiseオブジェクトが生成されてpendingの状態のものがresultに入り、それがログで出力されているということになります。

最後に

まだまだ深いところの理解はできていないですが表面的なところはこんな感じで理解できたのではないかと思います。
誤っているところ等ございましたらコメントにて教えていただけますと幸いです。

まだまだ分からないことが多すぎる。。。

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

JavaScriptでアバターに自動的にユーザーの名前が入れよう

Webアプリを作っていると、headerなどに表示されているアバターに画像を表示したいと言ったことが出てくると思います。

しかし、ユーザーの新規登録時などアバターの画像を設定していない段階で、何かしら代わりの画像を設定しておきたいことがあります。

そこで、今回はJavaScriptのsliceメソッドを使ってユーザー名を切り出し、イニシャルをアバターとして表示する方法について解説します。

完成形の確認

完成形は以下の画像の通りです。

スクリーンショット 2020-11-29 14.49.55.png

この画像ではユーザー名を'Guest User'としています。
イニシャルのGが表示されています。

例えば、ユーザー名がTaro YamadaならTJohnならJのように最初の1文字が表示されます。

実装

今回、ReactとFirebaseを用いて実装しますが、大切な部分はJavaScriptのsliceを使うところだけです。

上記の画像は以下のコードで実装しています。

HeaderRight.jsx
export default function HeaderRight() {
  const classes = useStyles();
  const { currentUser } = useAuth();
  const [userNameInitial, setUserNameInitial] = useState();

  useEffect(() => {
    const id = currentUser.uid;
    db.collection("users")
      .doc(id)
      .get()
      .then((snapshot) => {
        const data = snapshot.data();
        const username = data.username;
        const initial = username.slice(0, 1);
        setUserNameInitial(initial);
      });
  }, []);

  return (
    <div className={classes.headerRight}>
      <Link to="/postform" className={classes.postFormLink}>
        <Button className={classes.postFormButton}>新規投稿</Button>
      </Link>
      <Link to="/bookmarkList">
        <IconButton className={classes.bookMark}>
          <LibraryBooksIcon />
        </IconButton>
      </Link>
      <Link to="/dashboard" className={classes.avatarLink}>
        <Avatar aria-label="recipe" className={classes.avatar}>
          {userNameInitial}
        </Avatar>
      </Link>
    </div>
  );
}

ここで注目して欲しいのは、useEffectの中に書いてあるconst initial = username.slice(0, 1);です。

ここでusernameというのはデータベースから参照してしるユーザー名です。

そしてusernameに対して、slice(0, 1)とすることで、最初の一文字を切り抜けます。sliceの0は切り抜きの開始位置、1は何文字切り取るかを示しています。

例えば、文字列の先頭から2文字切り取りたいなら、slice(0, 2)とすればOKです。

あとは、切り抜いた文字をアバターとして表示するだけです。
※今回はMaterial uiのAvatarを使っています。

今回はuserNameInitialという変数に格納していますが、それを表示すれば完了です。

参考

String.prototype.slice()

先頭、末尾の文字を取得する方法

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

JavaScriptでアバターに自動的にユーザーの名前が入るようにしよう!!

Webアプリを作っていると、headerなどに表示されているアバターに画像を表示したいと言ったことが出てくると思います。

しかし、ユーザーの新規登録時などアバターの画像を設定していない段階で、何かしら代わりの画像を設定しておきたいことがあります。

そこで、今回はJavaScriptのsliceメソッドを使ってユーザー名を切り出し、イニシャルをアバターとして表示する方法について解説します。

完成形の確認

完成形は以下の画像の通りです。

スクリーンショット 2020-11-29 14.49.55.png

この画像ではユーザー名を'Guest User'としています。
イニシャルのGが表示されています。

例えば、ユーザー名がTaro YamadaならTJohnならJのように最初の1文字が表示されます。

実装

今回、ReactとFirebaseを用いて実装しますが、大切な部分はJavaScriptのsliceを使うところだけです。

上記の画像は以下のコードで実装しています。

HeaderRight.jsx
export default function HeaderRight() {
  const classes = useStyles();
  const { currentUser } = useAuth();
  const [userNameInitial, setUserNameInitial] = useState();

  useEffect(() => {
    const id = currentUser.uid;
    db.collection("users")
      .doc(id)
      .get()
      .then((snapshot) => {
        const data = snapshot.data();
        const username = data.username;
        const initial = username.slice(0, 1);
        setUserNameInitial(initial);
      });
  }, []);

  return (
    <div className={classes.headerRight}>
      <Link to="/postform" className={classes.postFormLink}>
        <Button className={classes.postFormButton}>新規投稿</Button>
      </Link>
      <Link to="/bookmarkList">
        <IconButton className={classes.bookMark}>
          <LibraryBooksIcon />
        </IconButton>
      </Link>
      <Link to="/dashboard" className={classes.avatarLink}>
        <Avatar aria-label="recipe" className={classes.avatar}>
          {userNameInitial}
        </Avatar>
      </Link>
    </div>
  );
}

ここで注目して欲しいのは、useEffectの中に書いてあるconst initial = username.slice(0, 1);です。

ここでusernameというのはデータベースから参照してしるユーザー名です。

そしてusernameに対して、slice(0, 1)とすることで、最初の一文字を切り抜けます。sliceの0は切り抜きの開始位置、1は何文字切り取るかを示しています。

例えば、文字列の先頭から2文字切り取りたいなら、slice(0, 2)とすればOKです。

あとは、切り抜いた文字をアバターとして表示するだけです。
※今回はMaterial uiのAvatarを使っています。

今回はuserNameInitialという変数に格納していますが、それを表示すれば完了です。

参考

String.prototype.slice()

先頭、末尾の文字を取得する方法

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

オンライン配信授業で、手を挙げたら【●●さん】とさしてくれるwebアプリを作ってみた

自己紹介

大学時代C言語を学んでから十数年以来のプログラミングを始めています。
本業はweb制作で、お客様から悩みや感じている課題についてに相談を受ける事も多いです。
LINEbotを中心に、webの技術を利用してどのようにお客様の悩みや課題を解決出来るのか、広く学んでいます!

今回は、Teachable Machineというgoogleが提供する機械学習が簡単に行えるウェブブラウザツールを知り、普段の生活に取り入れてみる事にしました!

【オンライン授業でのメンタルサポートAI】

実際に小学校中学年の子どもがオンラインで配信される塾の授業を受けているのですが、テレビ同様一方通行の授業なので、子供のやる気に波があるんです。
すぐに画面からいなくなったり、気が付いたら違う事をしていたり。。。
やっぱり対面での先生の問いかけがあったら、もっと積極的に授業に参加できるのになとは思いますが、このようなご時世なのでオンラインの方が安心ですよね。

家での経験だけではなく、オンライン授業が浸透してきいるなという実感が最近ありました。先日とある塾の保護者会で、通学に不安を感じる低学年向けに配信授業コースを開設するという話がありました。

その時に、低学年だとさらに親が管理をする負担が大きいな~と思っていた所、オンライン授業をサポートするAIがあったら面白い!と思いついたので早速実装してみる事にしました。

シュミレーションです。
こんな機能があったら良いなと思ってます。

image.png

子どもの表情や動きを機械学習して、状況を判断して問いかけます。

全体の流れ

今回は、機械学習初心者なので単純に

「手を挙げたらさしてくれる」

をまずはwebアプリとして実装します。

image.png

実際に作ったもの

webにアップしているのでクリックしていただくと体験できます

image.png

開発環境・利用ツール

・Teachable Machine
・Visual Code
・Line API
・node.js
・vue.js

Teachable Machineで画像サンプルを登録

Teachable Machine

Teachable Machineの使い方は【備忘録】Teachable Machineの利用方法で紹介しています。

1.手を挙げない
2.手を挙げる

この2パターンの画像を学習させます。

登録をしたらトレーニング

手を挙げていないあ/挙げたか
画像サンプルを撮りました。
image.png

トレーニングしてTeachable Machine上では成功!
image.png

#Visual Conde
いよいよ、node.jsを利用して、Teachable Machineで作成したモデルを利用して、web上で動作させるようにしたいと思います。

<body>
    <div class="wrap">
        <h1>わかったらてをあげてくださいね!</h1>
        <div id="app">
            <input v-model="name" placeholder="おなまえをおしえてください" size="30">
            <p>おななえ: {{name}}</p>
            <p>よろしく おねがいします</p>
            <p>わかったら てのひらがみえるように てをあげてくださいね</p>
            <p class="res">{{result}}</p>
            <video id="video" width="640" height="480" autoplay></video>
        </div>
    </div>
    <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
        const imageModelURL =
            'https://teachablemachine~始まるコードを入力します。';
        let video;
        // let classifier;

        const app = new Vue({
            el: '#app',
            data: {
                modelStat: 'モデルロード中...',
                detectedName: '見つかってない',
                name: '',
                result: '',
            },
            //最初の1度だけ実装
            async created() {
                // カメラからの映像取得
                const stream = await navigator.mediaDevices.getUserMedia({
                    audio: false,
                    video: true,
                });
                // html内のidがvideoのdomを取得
                video = document.getElementById('video');
                // videoにカメラ映像を適用
                video.srcObject = stream;

                // 自作モデルのロード
                classifier = await ml5.imageClassifier(
                    imageModelURL + 'model.json',
                    video,
                    modelLoaded
                );

                // モデルのロード完了時に実行される
                async function modelLoaded() {
                    console.log('Model Loaded!');
                    app.modelStat = 'モデルデータのロード完了';
                }
                async function interval() {
                    // 挙手しているかjudge
                    const myHandResults = await classifier.classify();
                    const myHand = myHandResults[0].label;

                    if (myHand == "手を挙げる") {
                        app.result = app.name + "さんっ!";
                    } else {
                        app.result = "";
                    }
                    console.log(myHand);
                }
                setInterval(interval, 3000); // 3秒ごとに処理
            },
        });
    </script>
</body>

今回ポイントとなったのは2か所

Vue.jsのフォーム入力バインディングを使って、入力した内容を簡単に出力しています。
Vue.jsのドキュメント

<input v-model="name" placeholder="おなまえをおしえてください" size="30">
<p>おななえ: {{name}}</p>

リアルタイム処理をしたいので3秒間隔で判断しています。

 setInterval(interval, 3000); // 3秒ごとに処理

さいごに

実装していて、色々なアイディアが出てきました。

また、顔の表情を認識して、眠そうであれば楽しそうな音楽を流す等反応するAIも面白いと思いました。ただ、表情のサンプルを子供から撮るのが難しくて失敗。

その他にも、百人一首の正誤判定する音声認識も企画しましたが、2秒しか音声サンプル入力できないので、出だしの「ありあけの~」で終わってしまうので、これも失敗。

結果として、今回は手を挙げているか挙げていないかだったので、高確率で判断してくれる機械学習を体験する事が出来ました。ポーズを使うと骨格が取れるので、より高度に認識できるみたいです。
機械学習を使って、一方通行のオンライン配信授業でも授業に参加しているような感覚を体験する事で、オンライン授業がより充実したものになっていくのではないかな~と思いました。

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

Progate JavaScript VI VII

コールバック関数という抵抗感を覚えそうな名前が出てきました。
理解するのにめちゃくちゃ時間かかりましたが(いうてもまだ50%くらいの理解。。)、こんなことが出来るならなんでも出来ちゃうんじゃないかって思いましたね!
コールバック関数等を最大限用いて、コードの効率化、処理の拡大化をさせて、様々なモノを作り上げることができるようになるのでしょうか・・
実際の成果物でどのような処理が書かれているのか気になってきますね!
もっともっと理解を深めたくなりましたし、やっぱり楽しいですね(´꒳`)

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

C#上でJavaScriptが動く | プリザンターでローコード開発

はじめに

ノーコード / ローコード開発が盛り上がってきていますね。プリザンターもごの分野の選択肢の一つとして活用してもらえるよう、色々と機能追加を進めています。

以前からプリザンターはJavaScriptを任意に追加してカスタマイズすることが出来ました。しかし、編集画面で計算を行うようなスクリプトを設置した場合、ユーザが直接編集画面を入力する場合に動作しますが、同じテーブルでインポートや、一覧画面で編集、APIで更新を行った場合などに動作させる事ができない問題がありました。

サーバサイドでJavaScriptを動かす

こうした問題を解決するために、サーバスクリプト機能を追加しました。この機能は、サーバ上でJavaScriptを動かし、計算処理や画面の表示制御などを簡単に実装できてしまう強力な機能です。以下のバージョンから使用することができます。

製品 バージョン
.NET Framework版 0.50.199.*
.NET Core版 1.1.9.0

どうやってやっているの?

プリザンターのサーバサイドは ASP.NET / C# で動いています。そのため、サーバサイドでJavaScriptを動かすといった機能を実装するためには、なんらかの拡張が必要です。プリザンターはで、下記のプラグインを使用してC#上でJavaScriptを動かせるようにしました。

https://www.nuget.org/packages/Microsoft.ClearScript/

設定方法

スクリプトを設置したい対象テーブルのテーブルの管理画面を開くと、「サーバスクリプト」タブが表示されます。こちらにスクリプトを追加していきます。

image.png

スクリプトの例1

// レコードの数値Aと数値Bを乗算して数値Cに格納する。
// JavaScriptの関数を使って四則演算以外の演算が可能。
// 「計算式の後」にチェック。
model.NumC = model.NumA * model.NumB;

スクリプトの例2

// 状況に合わせて進捗率を変更する。
// 条件分岐との組み合わせでプログラマブルな設定が可能。
// 「計算式の後」にチェック。
switch (model.Status) {
    case 150: // 準備
        model.ProgressRate = 10;
        break;
    case 200: // 実行中
        model.ProgressRate = 50;
        break;
    case 300: // レビュー
        model.ProgressRate = 90;
        break;
    case 900: // 完了
        model.ProgressRate = 100;
        break;
    default:  // その他
        model.ProgressRate = 0;
        break;
}

スクリプトの例3

// 分類Aと分類Bに値が入力されると状況を完了にする。
// 条件分岐との組み合わせでプログラマブルな設定が可能。
// 「計算式の後」にチェック。
if (model.ClassA !== '' && model.ClassB !== '') {
    model.Status = 900;
}

スクリプトの例4

// ユーザIDによって規定のビューを切り替える。
// 「サイト設定の読み込み時」にチェック。
if (context.UserId === 1) {
    siteSettings.DefaultViewId = 1;
} else if (context.UserId === 2) {
    siteSettings.DefaultViewId = 2;
} else {
    siteSettings.DefaultViewId = 3;
}

スクリプトの例5

// ユーザID: 1 以外の場合には、担当者が自分のものだけを表示するようフィルタする。
// 編集画面でも機能するため動的にレコードのアクセス制御を行うことが可能。
// 「ビュー処理時」にチェック。
if (context.UserId !== 1){
    view.Filters.Owner = '[' + context.UserId + ']'; 
}

今後の機能拡張

サーバスクリプト機能を利用すると、業務要件に合わせたアプリケーションをこれまで以上簡単に開発できるようになります。今後の機能拡張は下記のようなものを検討しております。

機能 説明
他レコードの操作 CRUDをサーバスクリプトから利用
項目のアクセス制御 利用者や状況に応じて項目の非表示や読み取り専用を設定
動的な通知 通知メソッドにより任意の条件やタイミングで通知を発行
一覧表示のCSS制御 任意の条件で行やセルにCSSを追加
ロック 任意の条件でレコードやテーブルをロック
メニュー制御 任意の条件でメニューの内容を変更する
コマンド制御 任意の条件でコマンドボタンの内容を変更する

今後も無料で使えるオープンソースのプリザンターの機能強化に取り組んでまいります。ぜひご期待ください!メリークリスマス!?

プリザンターのダウンロードはこちら。
https://pleasanter.org

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

配列から重複した値を取り除く方法 php &javascript

どのようなことがしたいか?

[1,4,3,1,3]のような配列から重複した値を取り除いて、[1,4,3]のような配列にしたい。

結論

php

array_unique()を使う

javascript

Setを使う


phpでは、array_unique()をいう関数を使うことで配列から重複した値を簡単に取り除くことができます
引数には対象の配列をいれますarray_unique(対象の配列)

qiita.php
$array=[2,3,2,4,3];
$array2=['a','b','c','a'];
$array=array_unique($array);
$array2=array_unique($array2);
//$array=[2,3,4]  $array2=['a','b','c']

一方javascriptではES2015(ES6)で導入されたSetを使います。これを使うことによって簡単に配列から重複した値を取り除くことができます。

qiita.js
let array=[1,3,5,1];
array=new Set(array);
console.log(array);
//(3)Set{1,3,5}
array=Array.from(array);
console.log(array);
//(3)[1,3,5]

Setを使うことによって重複を取り除いたオブジェクトを得ることができますそれを配列に直すにはArray.from()メソッドをしようします。

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