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

フワッとわかった気になるElm入門

はじめに

Elm は、JavaScript に似た構文を持つ Web アプリケーションを作るための言語です。
本記事では HTML や JavaScript の例を交えながら、Elm についてフワッとわかった気になってもらうことを趣旨としてます。

前提知識

  • HTML と JavaScript を触ったことがある
  • 変数や関数、配列、オブジェクトについて知っている

注意

Elm と JavaScript では構文が異なります。1 2
特に、関数定義と関数呼び出しでは、括弧とカンマでなく、スペースを使います。
また、return がないことも特徴です。

JavaScript
function sum (x, y) { // 関数定義
    return x + y;
}

sum(1, 2); // 関数呼び出し


function puls1 (x) {
    return x + 1;
}

plus1(sum(1, 2))
Elm
sum x y = x + y; -- 関数定義

sum 1 2 -- 関数呼び出し


plus1 x = x + 1;

plus1 (sum 1 2)

また、 Elm には JavaScript にない、型注釈というものがありますが、いつかまた説明しましょう。

sum : Int -> Int -> Int -- 型注釈
sum x y = x + y; -- 関数定義

view 関数

まずは、以下のリンクを開いて、 -- VIEW 以下を見てみましょう
https://ellie-app.com/9WqRmw9Hkf9a1

45行目
-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ button [] [ text "+" ]
        , div [] [ text "15" ]
        , button [] [ text "-" ]
        ]

このコードは、カウンターのボタンとカウンターの値を表示するだけです。
試しにボタンを触っても何も起きません。

view 関数で Web ページの見かけを作る

通常、Web ページに表示されるものは、HTML で記述されます。
例えば、ページのタイトル、次のページへのリンク、送信ボタン、など。

しかし、Elm では HTML の代わりに view 関数で Web ページを作ります。

view 関数に見出しを追加する

view 関数の中の div 関数や button 関数と、HTML の div タグや button タグを見比べると、よく似ていることがわかります。

HTML
    <div>
        <button>-</button>
        <div>15</div>
        <button>+</button>
    </div>
Elm
    div []
        [ button [] [ text "+" ]
        , div [] [ text "15" ]
        , button [] [ text "-" ]
        ]

試しに h1 関数で見出しを追加してみましょう。

-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ h1 [] [ text "Hello World" ]
        , button [] [ text "+" ]
        , div [] [ text "15" ]
        , button [] [ text "-" ]
        ]

HTML の属性

HTML の要素の関数以外にも、HTML の属性の関数もあります。3
例えば class 関数や、id 関数、style 関数などです。

HTML
<h1 class="title" id="firstTitle" style="color:red">Hello, World</h1>
Elm
h1 [ class "title", id "firstTitle", style "color" "red" ] [ text "Hello World" ]

Model

今度は、 -- MODEL 以下を見てみましょう
https://ellie-app.com/9WqRmw9Hkf9a1

18行目
-- MODEL


type alias Model =
    { count : Int }


init : Model
init =
    { count = 0 }

Model で、Web ページの状態を作る

view 関数で、Web ページの見かけを作りましたが、このままではカウンターに表示されるのは常に同じ "15" という定数です。
この定数を変数にして、値を更新できるようにするために、Elm では Model を利用します。

type alias Model =
    { count : Int }

Model は、レコードという JavaScript のオブジェクトのようなものです。4
また、レコードの中の変数はフィールドと呼びます。

この count というフィールドが、カウンターの値となります。

view 関数で Model から Web ページを作る

view 関数では、model という値を受け取っていました。
そして model.count でカウンターの値を取り出すことができます。

-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ button [] [ text "+" ]
        , div [] [ text "15" ]
        , button [] [ text "-" ]
        ]

view 関数の、text "15" の部分を、model.count に変えてみましょう。

-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ button [] [ text "+" ]
        , div [] [ text (String.fromInt model.count) ]
        , button [] [ text "-" ]
        ]

ちなみに String.fromInt 関数 は、数値 (Int) を 文字列 (String) に変換する関数です。5
また後で説明しますが Elm ではこのように、値の変換をキッチリとしなければなりません。

init 関数で、Web ページの最初の状態を作る

Web ページにアクセスしたときの、Model の最初の値は、init 関数で決めることができます。

-- MODEL


type alias Model =
    { count : Int }


init : Model
init =
    { count = 0 }

試しに、最初のカウントを10からに変えてみましょう

-- MODEL


type alias Model =
    { count : Int }


init : Model
init =
    { count = 10 }

まとめ

  1. view 関数で Web ページを作る
  2. view 関数から表示を変化させたい値(Web ページの状態)を抜き出して Model を作る
  3. view 関数で Model から Web ページを作るようにする
  4. init 関数で Model の最初の値を作る

Msg と update 関数

次に、 -- UPDATE 以下を見てみましょう
https://ellie-app.com/9WqRVH9bQjma1

31行目
-- UPDATE


type Msg
    = Increment
    | Decrement


update : Msg -> Model -> Model
update msg model =
    model

Msg で「ユーザーが操作した、その次に起きること」を作る

画面のフォームに対して、ユーザーから何か操作があったとき、「その次に起きること」を表現してみましょう。
Msg は「ユーザーが操作した、その次に起きること」を表すものです。
今回は
- +ボタンがクリックされたとき、「カウントが増える」
- -ボタンがクリックされたとき、「カウントが減る」
の2つです。

-- UPDATE


type Msg
    = Increment
    | Decrement

Increment は「カウントが増える」ということを表し、Decrement「カウントが減る」ということを表しています。

さて HTML のイベントが起きたときに、Msg の値が通知されるようにしましょう。

-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ button [] [ text "+" ]
        , div [] [ text (String.fromInt model.count) ]
        , button [] [ text "-" ]
        ]
-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Increment ] [ text "+" ]
        , div [] [ text (String.fromInt model.count) ]
        , button [ onClick Decrement ] [ text "-" ]
        ]

onClick 関数は、HTML のイベント属性である onclick を表しています。

そして、ここで通知された Msg の値が、次の update 関数に渡されます。

update 関数で「実際に何が起きるか」を作る

update 関数は、受け取った Msg に連動して「実際に何が起きるか」を表すものです。
具体的には、update 関数は、受け取った Msg によって分岐して Model を更新する関数です。

それでは
- 受け取った msg が Increment ならば model.count を1増やす
- 受け取った msg が Decrement ならば model.count を1減らす
この2つの処理を書いてみましょう。

-- UPDATE


type Msg
    = Increment
    | Decrement


update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

case msg of は JavaScript の switch 文のようなものです。6
msg が Increment のときと、Decrement のときで分岐することを表しています。

また、{ xxx | ~~ } はレコードを更新する構文です。
例えば、{ model | count = model.count + 1}は、 model.count + 1 を新しい model.count としたレコードを作る、という意味です。

ここで作られた新しい Model が view 関数に渡り、Web の表示が更新されます。

まとめ

おめでとうございます!
ついに、カウンターが動くようになりました。
https://ellie-app.com/9WqWzcqp3nra1

ここまでの手順をまとめてみましょう。

  1. view 関数からユーザーが操作した、その次に起きることを抜き出して Msg を作る
  2. view 関数で、HTML のイベントが起きたときに、Msg の値が通知されるようにする
  3. update 関数で、受け取った Msg によって処理が分岐し Model を更新する処理を作る

The Elm Architecutre

ここまでの一連の流れがつかめたでしょうか?

  1. init 関数で Model の最初の値を定めて、view 関数に渡される
  2. view 関数で 渡された Model によって Web ページが表示される
  3. ユーザーの操作があると Msg が通知され、update 関数に渡される
  4. update 関数で Msg によって処理が分岐し、 Model が更新されて view 関数に渡される
  5. 1.へ戻る

TEAPractice.png

このようなサイクルを繰り返して、画面が動くようになりました!

これが The Elm Architecutre です。
そんなに難しくないでしょう?

全体のまとめ

処理を作る流れ

  1. view 関数で Web ページを作る
  2. view 関数から表示を変化させたい値(Web ページの状態)を抜き出して Model を作る
  3. view 関数で Model から Web ページを作るようにする
  4. init 関数で Model の最初の値を作る
  5. view 関数からユーザーが操作した、その次に起きることを抜き出して Msg を作る
  6. view 関数で、HTML のイベントが起きたときに、Msg の値が通知されるようにする
  7. update 関数で、受け取った Msg によって処理が分岐し Model を更新する処理を作る

実際の処理の流れ

  1. init 関数で Model の最初の値を定めて、view 関数に渡される
  2. view 関数で 渡された Model によって Web ページが表示される
  3. ユーザーの操作があると Msg が通知され、update 関数に渡される
  4. update 関数で Msg によって処理が分岐し、 Model が更新されて view 関数に渡される
  5. 1.へ戻る

TEAPractice.png

完成品

https://ellie-app.com/9WqWzcqp3nra1

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

イベントハンドラー とイベントリスナーの違い(自分用メモ)

1.タグ内の属性として宣言する
2.要素オブジェクトのプロパティとして宣言する
3.addEventListenerメソッドを使って宣言する

引用元:JavaScript本格入門 山田祥寛著

1.2がイベントハンドラー
3がイベントリスナー

document.addEventListener('DOMContentLoaded',function(){
}, false);
内で定義されてるのが3のイベントリスナーってことにとりあえずしておく。

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

Kinx Tiny Typesetting - LaTeX 派? つか、知ってる?

Kinx Tiny Typesetting

こんにちわ。

今回は組版システムがメインです。TeX や LaTeXを使ってますか?それは良いですね。私はイマイマ 全く使ってません。好きですけど。

学生時代の論文書きには使ったものの、就職したら使わなくなってしまったあの懐かしくも美しいシステム、LaTeX。

この記事はそんな LaTeX に関係しつつ、私たちの Kinx に関連する、そんな内容です。

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。最近はだいぶ記事を書く時間がなく、生存確認的な記事ですが、ご容赦。書きたいことはいっぱいあるのですが。

組版?

まずはこちらのPDFファイルからご覧ください。

https://github.com/Kray-G/kinx/blob/master/examples/typesetting/typesetting.pdf

これは Kinx 組版ライブラリによって生成されました。なんだそれ、というのが今回のお話です。

このライブラリはまだ完成しておらず、極めて初期の段階にありますが、割りとイケてる感じに出力できたので、勇み足風に紹介したくなってしまった、という記事でありんす。

なぜ作り始めたか

PDF ライブラリ実装したんですよ。libharu ラップして。でもですね、生の PDF 操作するツール作るのも、まああるとは思いますが、座標計算したり面倒なので、ルールにしたがって組版する仕組みはほしいですよね。

そう思って見渡すと、そういうのって TeX くらいですよね。あと SATySFi とか SILE とか見つけたんですが、SATySFi も SILE も Windows が弱点のご様子(SILE はビルドできたけど出力が正しくなかった…)。まあ、SILE はすごく良さそうでしたが、数式はまだ未対応。数式が必要か?は議論の余地はありますが、TeX っぽい感じ!を意識すると欲しいところ。

というわけで、またしても車輪の再発明に走りました。特長としては「スモールサイズで手軽に組版」。面倒な作業は一切なし。ここだけは守りたい。

LaTeX は不要(使ってない)

ちなみに LaTeX は使っていません。というと語弊があるかもしれませんね。実際、LaTeX 自体はインストールすらしてません。が、数式に関しては KaTeX 内蔵でゴニョゴニョしてます。

LaTeX システムは非常に巨大なので、優れたシステムだとは思いますが、ちょっと PDF 作りたいなー程度の要望にインストールするには本格的すぎる気がして。

Kinx のスモール・パッケージをインストールしたら「なんかそれなりのが付いてた!」、くらいがちょうどいい感じなので、そんな感じを目指してます。

おわりに

これはまだ完成してないシステムの紹介です。やることはまだたくさんあるので、これからです。目次とか Book Style とか。最終的には簡易マークアップからの変換がまず当面のゴール。基本 Markdown からは行ける感じで。

まだまだ発展途上の段階ですが、興味があったり応援してくださる方がいれば、ぜひぜひ Github スターください!やる気出します。(ただ、完成形はリポジトリ独立させるかも…)

同梱するとフォントだけで Kinx 本体のサイズ越えそうなので、別パッケージでアドオンできるようにしたいですね。パッケージ・マネージャーが必要だ。

ではまた次回!

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

javascript関数ドリル 初級編uniq関数の実相のアウトプット

uniq関数の課題内容

詳細はこちら
   ↓
https://js-drills.com/blog/without/

uniq関数の取り組む前の状態

uniqメソットがどんなものか分からない状態

uniq関数に取り組んだ後の状態

自力でできた

uniq関数の実装コード(答えを見る前)

分からなった

 if( !values.includes(candidateToPush) )

が分からなかった

uniq関数の実装コード(答えを見た後)

function without(array, ...values) {
  const newArray = [];
  for(let i = 0; i < array.length; i++) {
    const candidateToPush = array[i];
    // values : [1, 2]
    // array: [2, 1, 2, 3]
    // candidateToPush: 2, 1, 2, 3
    if( !values.includes(candidateToPush) ) {
      newArray.push(candidateToPush);
    }
  }

  return newArray;
}

console.log( without([2, 1, 2, 3], 1, 2) );
//[3]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Three.jsでgltfを読み込んだ時に、カリングを有効にする。

カリングとは

面の片側のみ描画して負荷を下げる方法。
サイコロを現実世界に置いてみればわかると思うが、必ず6面あるうちの3面しか同時に見ることができない。そのため、裏側に位置する3面を描画しないようにする仕組みのこと。

three.jsのデフォルトでは、materialから、表示するsideを指定することになる。

やり方

blenderで出力したものを読み込んだ時に、カリングがうまくONにならなかった。
多分loader側ではあんまりいじってないので、自前ですべてカリングを有効にしておこう。

(new THREE.GLTFLoader()).load( 'model.glb' , gltf => {
    //キューにすべてのchildrenをいれる。
    const targets = [...gltf.scene.children];

    while(targets.length > 0) {
        let child = targets.pop();
        for(let cc of child.children) {
            targets.push(cc);
        }
        if(child.type == "Mesh" && child.material) {
            child.material.side = THREE.FrontSide;
        }
    }
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascript関数ドリル 初級編without関数の実相のアウトプット

zipObject関数の課題内容

詳細はこちら
   ↓
https://js-drills.com/blog/without/

zipObject関数の取り組む前の状態

スプレッド演算子を使う発想がなかった。includeメソットを知らなかった

zipObject関数に取り組んだ後の状態

理解できた

zipObject関数の実装コード(答えを見る前)

分からなかった

zipObject関数の実装コード(答えを見た後)

function without(array, ...values) {
  const newArray = [];
  for(let i = 0; i < array.length; i++) {
    const candidateToPush = array[i];
    // values : [1, 2]
    // array: [2, 1, 2, 3]
    // candidateToPush: 2, 1, 2, 3
    if( !values.includes(candidateToPush) ) {
      newArray.push(candidateToPush);
    }
  }

  return newArray;
}

console.log( without([2, 1, 2, 3], 1, 2) );
// => [3]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript range

function range(start, stop)
{
    var array = [];

    var length = stop - start; 

    for (var i = 0; i <= length; i++) { 
        array[i] = start;
        start++;
    }

    return array;
}

console.log(range(1, 7)); // [1,2,3,4,5,6,7]
console.log(range(5, 10)); // [5,6,7,8,9,10]
console.log(range(-2, 3)); // [-2,-1,0,1,2,3]

utils.jsに

export function range(start, stop)
{
  const array = isNumber;

  const length = stop - start;

  for (let i = 0; i <= length; i++) {
    array[i] = start;
    start++;
  }

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

Javascriptにrangeを入れる方法

Nuxtjsプロジェクトにpythonのrange()のようなやつを入れたかったので以下のとおりで

utils.jsに

export function range(start, end) {
  /* generate a range : [start, start+1, ..., end-1, end] */
  const len = end - start + 1;
  const a = new Array(len);
  for (let i = 0; i < len; i++) a[i] = start + i;
  return a;
}

使うときは range(26, 52)
そのままcomponentに追加する時は
range(start, end) {
/* generate a range : [start, start+1, ..., end-1, end] */
const len = end - start + 1
const a = new Array(len)
for (let i = 0; i < len; i++) a[i] = start + i
return a
}
})

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

コンポーネント間のデータのやりとりを簡単なTODOアプリでまとめる

Vuexに頼りすぎてコンポーネント間のデータの受け渡し方が曖昧だったので、簡単なサンプルアプリでまとめる

また、Vueで開発をしていてコンポーネントを細かく分けない事で以下の問題がよく起こったのでその反省

コンポーネントを分けない問題
・汎用性の悪さ
例えばフォームとボタンを一緒のコンポーネントに作ると、ボタンだけ使い回したい時にフォームまでついてきて汎用性が悪い。

・同じような記述をしたコンポーネントファイルが増えDRYに反する

・1ファイルのコード量が増えて可読性が悪い

作成したコンポーネント

・ボタン
・入力フォーム
・ボタンと入力フォームをまとめたコンポーネント

ただ単にクリックイベントとボタン名を使いまわせるボタン

button.vue
<template>
  <button @click="onClick">{{name}}</button>
</template>
<script>
export default {
  props:{
    name:{
      type:String,
      default:"button"
    },
    onClick:{
      type:Function,
      required:true
    }
  },
}
</script>

入力フォーム。
propsの値はv-modelで直接変更するとエラーになる
computedでv-modelの変更を検知し
親コンポーネントへ入力された値(input)を送り、親側でpropsの値を更新する

Input.vue
<template>
<input type="text" v-model="input">
</template>
<script>
export default {
  props:{
    inputValue:String
  },
  computed:{
    // v-modelのinputの変更を検知
   input:{
     get(){
       return this.$props.inputValue
     },
    //  親コンポーネントにinputを送り出して親側でpropsの値を書き換える
     set(value){
        this.$emit("setValue",value)
     }
   }
  }
}
</script>

ボタンと入力フォームが存在するコンポーネント
入力フォームの値をボタンクリックで配列に格納し親コンポーネントへ渡す

Form.vue
<template>
  <div>
    <!-- $emitで渡ってきたイベントを実行 -->
    <!-- 子コンポーネントのpropsに$emitの引数で受け取った値をセット -->
    <Input
    @setValue="setValue"
    :inputValue = value />
    <!-- Buttonコンポーネントのイベント発火とボタン名を設定 -->
    <Button
    :onClick="postTodo"
    name="add"
     />
  </div>
</template>
<script>
import Button from "@/components/Button.vue"
import Input from "@/components/Input.vue"
export default {
  data(){
    return{
      value:null,
      todos:[]
    }
  },
  components:{
    Button,
    Input
  },
  methods:{
    postTodo(){
      this.todos.push(this.value)
      this.value=""
   //todoが追加された配列を親コンポーネントへ渡す
      this.$emit("setTodo",this.todos)
    },
    setValue(value){
      this.value=value
    }
  }
}
</script>

Form.vueから受け取った配列todosをv-forでレンダリング

App.vue
<template>
  <div id="app">
    <h1>Todo</h1>
    <Form 
    @setTodo="setTodo"
    />
    <hr>
    <template v-for="(todo,index) in todos">
      <li :key="index">{{todo}}</li>
    </template>
  </div>
</template>
<script>
import Form from "@/components/Form.vue"
export default {
  name: 'App',
  components: {
    Form
  },
  data(){
    return{
      todos:null
    }
  },
  methods:{
    setTodo(todos){
      this.todos=todos.reverse()
    },
  }
}
</script>

まとめ

・v-modelの値を渡したい時はcomputedで変更を検知して$emit経由で値を渡して親側からpropsを更新する
・$emitでどんどん親へ親へ渡していく。
・兄弟のデータを使う時は親のdataに保管したものを使う

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

株価指数の下落率に応じてLINE通知する。

積立投資をしているのですが、暴落時にはプラスで積立するのがより効率が良いです。
とある動画で、S&P500が先週対比にて-5%以上の時は買い増すチャンスということを知り、
S&P500の下落率が一定の基準値以下になった場合は、LINE通知できる様に実装しました。

前提:
・実行環境:GAS
・LINE notify にてTOCKEN取得済み。
LINE通知等で検索すると、LINETOCKENの取得方法等確認が出来ます。
・S&P500の値は下記APIを利用
https://financialmodelingprep.com/developer/docs/
*1

仕様:
S&P500の終値が1週間前に比べて4%以上落ちていたらLINEに通知する。

実際のコード

//発行したtockenを利用
var lineToken = "***************";

function main() {
//発行したAPIkeyを利用
  var apiUrl = "https://financialmodelingprep.com/api/v3/historical-price-full/%5EGSPC?apikey=**************";

  var apiOptions = {
     method : 'get'
  };

  var responseApi = UrlFetchApp.fetch(apiUrl, apiOptions);
  var responseJson = JSON.parse(responseApi.getContentText());

  var yesterDay = responseJson.historical[0].date
  var yesterDayClosePrice = responseJson.historical[0].close

 var lastWeekDay = responseJson.historical[4].date
 var lastWeekDayPrice = responseJson.historical[4].close


  const declineRation = ((yesterDayClosePrice/lastWeekDayPrice)-1)*100;

  const disPlayRation = Math.round(declineRation * 10) / 10

  //if (declineRation <= -4) {
    var message = "チャンス! \n S&P500 の対先週終値が " + disPlayRation + ' % ' +'です!'
    + '\n 先週: '+ JSON.stringify(lastWeekDay) + ' 終値 :' + JSON.stringify(lastWeekDayPrice)
    + '\n 先日: '+ JSON.stringify(yesterDay)+ ' 終値: ' + JSON.stringify(yesterDayClosePrice);
    console.log(message);
    sendToLine(message);
  //}
}

function sendToLine(message) {
  var token = lineToken;
  var options = {
    "method": "post",
    "payload": {"message": message},
    "headers": {
      "Authorization": "Bearer " + token
    },
    "muteHttpExceptions" : true,
  };
  UrlFetchApp.fetch("https://notify-api.line.me/api/notify", options);
}

*1
公式サイトにある様に無料で叩ける回数が250回まで。
https://financialmodelingprep.com/developer/docs/pricing/

困った点
困った点が一番が無料でS&Pの終値提供APIが全く見つからなかったことです。
有名どころだと
Quandl
alphavantage

あたりですが、提供しているのが、月毎だったり、そもそも提供していなかったり。。。。

個別株は簡単に見つかるので需要があまり無いんですかね。。。

週に一度の計算のため1.2年は無料回数範囲ですが、
超える前に良いAPI見つけておきたいです。

ちなみに、ライブラリを使うのが一番簡単ですね。

今回はGASを利用したかったので、APIでデータ取得できるものを探していましたが、
Python×herokuでも断然ありだと思います。

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

javascript関数ドリル 初級編zipObject関数の実相のアウトプット

zipObject関数の課題内容

詳細はこちら
   ↓
https://js-drills.com/blog/zipobject/

zipObject関数の取り組む前の状態

関数は断片的にはわかっているが、どんな関数の種類などわからない状態

zipObject関数に取り組んだ後の状態

オブジェクトに文字列を入れるときは []になることを忘れていたので思い出せてよかった

zipObject関数の実装コード(答えを見る前)

自力では全然わかりませんでした

zipObject関数の実装コード(答えを見た後)

function zipObject(props = [], values = []) {
  const zippedObject = {};
  for(let i = 0; i < props.length; i++) {
    const prop = props[i];
    const value = values[i];
    zippedObject[prop] = value;
  }

  return zippedObject;
}

console.log( zipObject(['a', 'b'], [1, 2]) );
// => { 'a': 1, 'b': 2 }

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

GASのcalendarで「CalendarApp.Calendar.createEventのメソッドのシグネチャと一致しません」というエラーが出る問題の解決方法

CalendarApp.Calendar.createEventのメソッドのシグネチャと一致しません。

というエラーが出てしまう問題について、原因を調べた所

createEventの第2引数、第3引数のデータ型は本来Dateオブジェクトである必要がある。

自分はここで文字列形式のデータになってしまっていたので、date型に修正すると
期待通り動作した。

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

Sequelize で名前を指定してmigrationを実行する

sequelize db:migrate:status // でmigration nameの一覧表示
sequelize db:migrate --name <migration name> // migration 実行
sequelize db:migrate:undo --name <migration name> // rollback
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sequelize で名前を指定してmigrationをrollbackする

sequelize db:migrate:status // でmigration nameの一覧表示
sequelize db:migrate:undo --name <migration name> // rollback
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

input type="file" をまともにする

ファイルアップロードのUI(input type="file")は厄介です。ブラウザによって表示方法が異なるのにCSSでのカスタマイズが難しいし、Chrome以外では添付したファイルをキャンセルできないという問題もあります。ですが、JavaScriptを使えば、

  1. CSSでカスタマイズ可能
  2. 添付したファイル名を表示可能
  3. 添付したファイルをキャンセル可能

にできます。

元のHTML

    <form method="POST" enctype="multipart/form-data">
        <ul>
            <li><input type="file" name="file1"></li>
            <li><input type="file" name="file2"></li>
        </ul>
        <input type="submit" value="UPLOAD">
    </form>

input type="file" な要素が2つあります。なのでJavaScriptでコントロールするときには対象を特定する必要があります。

修正版HTML

よくあるのは input type="file" な要素を非表示にした上で label 要素で囲み、label のクリックで input type="file" のポップアップを連動させるというやり方です。ですが、JavaScript を使うのであれば label にこだわる必要はありません。むしろ label にはデフォルトで連動のアクションがあるため扱いが面倒です。

label の代わりに span で囲むことにします。目印のために class="upload" としています。

    <form method="POST" enctype="multipart/form-data">
        <ul>
            <li><span class="upload">
                <input type="file" name="file1">
                <input disabled>
                </span></li>
            <li><span class="upload">
                <input type="file" name="file1">
                <input disabled>
                </span></li>
        </ul>
        <input type="submit" value="UPLOAD">
    </form>

span の中には2つ input 要素があります。一つは元々の input type="file" な要素、もう一つはファイル名を表示するために追加した要素です。追加した要素は表示のためだけに使うので、disabled にしています。アイコンなどを追加したい場合は span 要素の中に入れればクリック時にポップアップと連動します。

追加のCSS

以下のCSSを追加します。

    form .upload {
        display: inline-block;
    }
    form .upload input[type="file"] {
        display: none;
    }
    form .upload input[disabled] {
        pointer-events: none;
    }

span 要素はクリックを「受け止める」必要があるため、inline-block にします。元々の input 要素は非表示にし、追加した input 要素はクリックを「素通し」するので pointer-events: none; とします。この設定がないと Firefox では追加した input 要素がクリックを「消費」してしまい、span までクリックが伝わりません。

コントロール用JavaScript

以下のJavaScriptを追加します。

    $(function(){
        $('.upload').on('click', function(){
            $(this).find('input').val('');
            $(this).find('input[type="file"]').trigger('click');
        });
        $('.upload input[type="file"]').on('click', function(event){
            event.stopPropagation();
        });
        $('.upload input[type="file"]').on('change', function(){
            if (this.files.length) {
                $(this).parent().find('input[disabled]')
                    .val(this.files[0].name);
            }
        });
    });
  1. span 要素(class="upload")がクリックされたときは、内部にある2つの input 要素をともにクリアし、input type="file" な要素をクリックすることでポップアップを起動します。
  2. input type="file" な要素がクリックされたときに親要素へのイベント伝播を停止します。これを行わないと再度 span がクリックされることになるので無限ループになってしまいます。
  3. ポップアップから戻ったとき、ファイルが選択されていればそのファイル名を追加した方の input 要素に表示します。

jQueryを使ったので簡潔に書けました。生のDOM操作関数でも記述可能と思いますが、かなり面倒になると思います(私にはその根気はありません)。

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

javascrpitで自動更新処理

60秒で自動更新

const timer = 60000    // ミリ秒で間隔の時間を指定
window.addEventListener('load',function(){
  setInterval('location.reload()',timer);
});

イベント実行時に画面も更新

  • /stopへリクエストして、5秒後に画面リロード
    handleClickStop() {
        const request = axios.create({
            baseURL: "http://localhost:8080"
        })
        request.post("/stop")
        setTimeout(window.location.reload(),5000);
    }

参考

https://designsupply-web.com/media/knowledgeside/4431/

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

Javascriptでリロード処理

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

vue/composition-apiでscrollの状態を扱うComposition Functionを作った

概要

上向きにスクロールしたときに表示して、下向きにスクロールしたら隠すフッターを表示する要件が現れたので、useScrollを作成して、スクロールの向きを絶えずリアクティブに返すComposition Functionとして扱ってみました。

ソースコード

import { useWindowScroll } from '@vueuse/core'
import {
  reactive,
  toRefs,
  watch,
} from '@vue/composition-api'

export const useScroll = () => {
  const { x, y } = process.browser ? useWindowScroll() : { ...toRefs(reactive({x: 0, y: 0})) }

  const state = reactive<{
    isUp: boolean,
    isDown: boolean
  }>({
    isUp: false,
    isDown: false,
  })

  watch(() => y.value, (newY, oldY) => {
    state.isUp = newY < oldY
    state.isDown = newY > oldY
  })

  return {
    x,
    y,
    ...toRefs(state),
  }
}

解説

こちらのFunctionには、vueuseというライブラリを使わせてもらっています。
composition-apiを使った、特にブラウザのネイティブAPIで扱える値に関してリアクティブに活用できるFunctionがたくさんあります。

今回はuseWindowScrollを利用させていただきました。
これは、windowへのEvent Listenerとして、ステート管理しているスクロールの高さを変更するハンドラを登録していることによって、リアクティブに座標の値を管理できるようにしています。

composition-apiのwatchで、スクロールのy値を監視しており、変化したときの古い値との比較によって上昇中か、下降中かを判定しています。

使い方

こんな感じで書くと、スクロールの向きに応じて出たり消えたりするフッターが作れます。高さが70px決め打ちになっているのが少し悔やまれますが、Vue3でStyle周りの改善が入るらしいのでちょっとそれを心待ちにしていようと思っています。

<template>
  <footer :style="footerStyle" class="sync-scroll" :class="{ appear: isUp }">
    <slot />
  </footer>
</template>

<script lang='ts'>
import {
  computed,
  defineComponent,
} from '@nuxtjs/composition-api'
import { useScroll } from '~/composables/utils/window/useScroll'

export default defineComponent({
  setup() {
    const scrollState = useScroll()

    const footerStyle = computed(() => {
      if (scrollState.isUp) {
        return {
          height: '70px',
        }
      }

      return {
        bottom: '-70px',
        height: '70px',
      }
    })

    return {
      ...scrollState,
      footerStyle,
    }
  },
})
</script>

<style lang='scss' scoped>
@import '@/assets/css/variable.scss';

.sync-scroll {
  width: 100%;
  position: fixed;
  bottom: -70px;
  animation-name: hide;
  animation-duration: .4s;
  animation-timing-function: linear;

  &.appear {
    bottom: 0;
    animation-name: appear;
    animation-duration: .4s;
    animation-timing-function: linear;
  }
}

@keyframes appear {
  0% {
    bottom: -70px;
  }
  100% {
    bottom: 0;
  }
}
@keyframes hide {
  0% {
    bottom: 0;
  }
  100% {
    bottom: -70px;
  }
}
</style>

注意点

もとのuseWindowScrollが、スクロールのイベントハンドラにthrottleのような処理を噛ませていないっぽくて、全てのスクロールイベントに対してハンドラを呼び出しているようです。パフォーマンスを重視するならthrottleを噛ませたほうが良いと思います。

最後に

composition-apiにハマって日々いろいろ試して発信しているTwitterアカウントはこちらです。
https://twitter.com/Meijin_garden

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

自分用メモ axios javascript

はじめに

今回は、javascriptのライブラリであり、HTTP通信を簡単に行えるaxiosについて基本だけ勉強したのでここでアウトプットしておきたいと思います。
プログラミング初心者のアウトプットですので、至らない点があると思いますので、暖かく見ていただければと思います。また違った点などあればご指摘していただければ幸いです。

GET通信

axios.get('https://...').get.then(reponse => response.data).catch(err => console.log(err)

//configオブジェクトを使うとき
axios({
  method: 'GET',
  url: 'https://...'
}).then(reposne => ...

これが基本の形になる。

パラメータ付き

const params = {name: 'yuuki'}
axios.get('https://...', {params}).then(response => ...
//configオブジェクトの場合
axios({
  method: 'GET',
  url: 'https://...'
  params: {name: 'yuuki'}
}).then(response => ...

POST通信

const data = {firstName: ..., lastName: ...}
axios.post('https://...',data).then(response => ...
//configオブジェクトの場合
axios({
  method: 'POST',
  url: 'https://...'
  data: {firstName: ..., lastName: ...}
}).then(response => ...

レスポンスの構造

axios使用後のレスポンスの構造は、以下

data, status, statusText, headers, config

dataのなかにgetしたい内容が入ってくる。(body)

バイナリーデータ

バイナリーデータとは、テキストデータ以外のデータになる。
厳密にいうと、コンピューターが見てわかるデータ

デフォルトはJSONで返ってくる。fetchのようにjson()メソッドを利用しなくても最初からJSON形式で返ってくる。

そしてバイナリーデータでのレスポンスを取得したい場合は,arrayBufferを使う

responseType: 'arrayBuffer'

をヘッダーに追加する。

エラーハンドリング

axionsのエラー表示はステータスコードが200台以外で帰ってきた場合は、エラーを返すようになっている。
error.responseという形で帰ってくる。

if文などでerror.responseが帰ってきたときの処理などを書いておくことがマナー。

しかしdefaultのエラー表示から変えることもできる
例えば、、、

validationStatus: function(status){
  return status > 500
}

こうすると500以外は成功として認識され、axionsではthenの中で処理ができる。

他にもオプションでタイムアウトやカスタムヘッダなども追加できる

timeout: 1000

//カスタムヘッダー

header: {X-SPECIAL-TOKEN: "abcde"}

追加方法もこれまでのheaderやパラメータなどと一緒!

Basic認証

Basic認証とは、webサーバに付随している機能の一つでユーザー名とパスワードを入力するようにすること。これを知っていないとそのページにアクセスできないようになっている。

authオプションを使う

auth: {username: 'userName', passoword: 'pass1'}

のように使う!!

まとめ

以上がaxiosについての今回の記事になります。基本だけしか理解しておらず深い知識などはまだまだなためこの記事も更新していければと思っています。何か至らない点がありましたら、はじめにも言いましたが、ご指摘していただけるとありがたいです。

参照

[フロントエンド] axiosライブラリを使って、柔軟にHTTP通信を行う

axiosの使い方まとめ (GET/POST/例外処理)

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

(小ネタ)discord.jsでメッセージから絵文字を取得する

はじめに

備忘録も兼ねて、discord.jsにてメッセージからUnicode絵文字を判別し、取得する方法を紹介します。
DiscordにおいてUnicode絵文字とは、各サーバーで登録されるカスタム絵文字以外の、初めから全てのユーザーができる絵文字のことです。
image.png
Discordでは絵文字にTwemojiを使用しており、それゆえにTwemojiと互換性があります。
そのため今回はTwemojiの絵文字判定処理に使用されているtwemoji-parserを用いたUnicode絵文字判定処理の方法を紹介します。
twemoji-parserはtwemoji同様、Twitter社によって管理されているライブラリなので、安心感があります。

事前準備

適当にプロジェクトを用意していただき、twemoji-parserをインストールしておきます。

npm install twemoji-parser

本題

今回はサンプルとして、message イベントで受け取ったメッセージ内に含まれるUnicode絵文字を、リアクションとして返すだけのコードを紹介します。
コードとしてはシンプルなため、解説は省かせて頂きます。

const { Client } = require('discord.js');

// twemoji-parserから判定用の正規表現を取得(gオプション付き)
const twemojiRegex = require('twemoji-parser/dist/lib/regex').default;

const client = new Client();

client.on('message', message => {
  // メッセージから正規表現でUnicode絵文字を取得
  const mathEmojis = message.content.match(twemojiRegex);

  for (const emoji of mathEmojis) {
    // 取得したUnicode絵文字をリアクションで返す
    message.react(emoji)
      .catch(console.error);
  }
});

client.login('Your bot token')
  .catch(console.error);

こんな感じで動作します。
image.png

注意点

twemoji-parserの正規表現にはgオプションが付いています。
ですから、使用目的によってはそのまま利用できないため、自前で正規表現を再生成する必要があります。
例えば、単にgオプションを外したい場合、このようにします。

const twemojiRegex = require('twemoji-parser/dist/lib/regex').default;
const onceRegex = new RegExp(twemojiRegex.toString().slice(1, -2));

※スマートな方法が思いつかなかったので、より良い方法があれば教えてください。

あとがき

メッセージ内のUnicode絵文字を判別し取得する処理は、自前で書こうと思うとなかなかの至難の技です。
twemoij-parserを使用すればUnicode絵文字の複雑な組み合わせも、しっかり1文字と判定されるため、複雑なことを考えずに済みます。
また、これを応用すればスプレッド構文でも実現できなかった、実態に即したUnicodeのパースも行えそうです。

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

Hooks(useRef) + TypeScriptで親から子コンポーネントの関数を発火する

前提

Reactにおいて親コンポーネントから子コンポーネントを操作(DOMをさわる、関数を発火させる)のはアンチパターン。

いつもの話ですが、ref を使った手続き的なコードはほとんどの場合に避けるべきです。
by https://ja.reactjs.org/docs/hooks-reference.html#useimperativehandle

refを使わずに実装するのであれば、状態を親コンポーネントに寄せるか、あるいはReduxやRecoilに頼るしかない。だけど、それでもどうしてもやりたいという場合は下記の通り。

親コンポーネント

import React, { useRef } from 'react';

const Parent = () => {
  const ref = useRef(null); // 初期化

  const hogeFunction = () => {
    ref.current.fire(); //refオブジェクトからfire()を実行
  }

  return (
    <Child ref={ref}> // refオブジェクトをそのまま渡す
  )
}

子コンポーネント

import React, { FC, useImperativeHandle } from 'react';

interface Props {
  ref: RefObject<void>;
}

const Child: FC<Props> = ({
  ref
}) => {

  useImperativeHandle(ref, () => ({
    fire() {
      // 発火させたい処理
    }
  }));

  // ...(省略)...
}

useImperativeHandleの使い方は下記の通り。

useImperativeHandle(ref, createHandle, [deps])

参考

https://ja.reactjs.org/docs/hooks-reference.html

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

JavaScriptの変数定義

JavaScriptの学習を始めたのでその日学んだことをアウトプットしていきます。

JavaScriptとはプログラミング言語の1つで、ページを移動せずに画面の表示を切り替えたり

画面を更新せずに、サーバーと通信することができます。

本題の変数の定義方法ですが、var、const、let、といった3つ存在します。

① var

varとは再定義、再代入が可能な書き方です。

例(定義方法)
スクリーンショット 2020-09-09 21.28.51.png

console.log(sample)とするとおはようが出力されます。
スクリーンショット 2020-09-09 21.30.47.png

また、再代入が可能でこうすると代入できます。
スクリーンショット 2020-09-09 21.33.40.png

また、再定義も可能で
スクリーンショット 2020-09-09 21.34.15.png
と再定義すると
スクリーンショット 2020-09-09 21.34.30.png

このような結果になります。

ただし、varは現在開発現場においてあまり使用されておらず、下記の2つがよく使用されるそうです。

②const

constは後から書き換えることのできない変数を定義する書き方です。
ただし再代入、再定義は共に不可です。

例(定義方法)
スクリーンショット 2020-09-09 21.40.15.png

出力
スクリーンショット 2020-09-09 21.41.13.png

再代入、再定義共に不可なのでこのようにエラーが表示されます。
スクリーンショット 2020-09-09 21.43.38.png

③let

letは後で書き換えることができる変数を定義する書き方になります。
再代入は可能ですが、再定義は不可です。


スクリーンショット 2020-09-09 21.47.52.png

出力
スクリーンショット 2020-09-09 21.48.17.png

代入
スクリーンショット 2020-09-09 21.49.16.png

とこのようになります。

参照
https://techacademy.jp/magazine/14872

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

Next.js+TypeScript+AWS Amplify+RecoilでToDoリストを作る

本記事ではNext.js+TypeScript+AWS Amplify+Recoilを使って、モダンなToDoリストを作る方法を紹介します。

image.png

作ったアプリケーションは公開しています。
https://master.d182t7iqbd44r9.amplifyapp.com

Githubリポジトリを公開しますので、不具合や不適切な実装を見つけた場合はドシドシIssueかPull-Requestいただけると幸いです。

https://github.com/yuuu/next-ts-amplify-recoil-todolist

背景

私自身普段はRuby on Railsを使って開発しています。JavaScriptは正直まだ苦手です。

Railsは爆速でアプリを開発出来る点が魅力的ですが、一方でモバイルアプリとの連携やリッチなUIが求められる案件では、フロントエンドとバックエンドを分離した構成にせざるをえないケースがあります。
そのような構成だと、かえってRailsがリッチ過ぎるとも感じており、AWS Amplifyのようにバックエンドをスピーディーに構築してくれるサービスを一度使ってみたいと思っていました。

そのため、Next.js+AWS Amplisyで簡単なアプリを作ってみて、いわゆるモダンなアプリ開発のノウハウを学んでみることにしました。

使用する技術要素

  • Next.js(React.js)
  • AWS Amplify
  • TypeScript
  • Recoil
  • React Hook Form

プロジェクトの作成

Next.jsのexamplesにwith-typescript-eslint-jestが公開されているので、これをベースにしました。

$ create-next-app
✔ What is your project named? … next-ts-amplify-recoil-todolist
✔ Pick a template › Example from the Next.js repo
✔ Pick an example › with-typescript-eslint-jest
# 省略

Amplifyのセットアップ

続いて、AWS Amplifyをセットアップします。

初期化

amplify init を実行して初期化します。

$ cd next-ts-amplify-recoil-todolist
$ amplify init
? Enter a name for the project todolist
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you\'re building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path: src
? Distribution Directory Path: out
? Build Command:  npm run-script build
? Start Command: npm run-script start
? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use (使用するprofile)

Hostingのときにハマらないよう、Distribution Directory Pathは out にしています。
詳細は こちらの記事 を参照ください。

ホスティング(CI/CD含む)

経験上、後からホスティングを追加するとトラブルシューティングが難しくなるので、できるだけ早い段階でCI/CDによるホスティングを設定しておきます。

❯ amplify add hosting
? Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
? Choose a type Continuous deployment (Git-based deployments)
? Continuous deployment is configured in the Amplify Console. Please hit enter once you connect your repository
# ブラウザでAWS Amplify Consoleが表示されるので諸々設定をした後、Enterを入力
Amplify hosting urls: 
┌──────────────┬──────────────────────────────────────────────┐
│ FrontEnd Env │ Domain                                       │
├──────────────┼──────────────────────────────────────────────┤
│ master       │ https://master.d14mfq14xzgfle.amplifyapp.com │
└──────────────┴──────────────────────────────────────────────┘

build の出力先を out に変更するため、手元の環境で package.json を一部修正する必要があります。
詳細は こちらの記事 を参照ください。

また、Amplify Consoleの「ビルドの設定」からamplify.ymlを次のように修正します。

amplify.yml
version: 1
backend:
  phases:
    build:
      commands:
        - '# Execute Amplify CLI with the helper script'
        - amplifyPush --simple
frontend:
  phases:
    preBuild:
      commands:
        - yarn install
    build:
      commands:
        - yarn run build
  artifacts:
    baseDirectory: out
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*

さらに、「ビルドの設定」→「Build image settings」→「Edit」→「Live package updates」でAmplify CLIlatest とする必要があります。これをしていないと、デプロイに失敗するので要注意です。

image.png

デプロイが終わり、表示されたURLにアクセスすると、ページが表示されます。

image.png

認証基盤

以下コマンドで認証基盤を追加します。

$ amplify add auth
? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No, I am done.
# 省略

$ amplify push
✔ Successfully pulled backend environment production from the cloud.

Current Environment: production

| Category | Resource name        | Operation | Provider plugin   |
| -------- | -------------------- | --------- | ----------------- |
| Auth     | besttodolistf818478c | Create    | awscloudformation |
| Hosting  | amplifyhosting       | No Change |                   |
? Are you sure you want to continue? Yes
# 省略

GraphQL API

予めGraphQLのスキーマファイルを作成しておきます。

./schema.graphql
type Todo @model {
  id: ID!
  name: String!
  completed: Boolean!
  timestamp: AWSTimestamp!
}

以下コマンドでバックエンドのGraphQL APIを追加します。

$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: todolist
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? Yes
? Provide your schema file path: ./schema.graphql
# 省略

念の為mockサーバを起動して動作確認をしておきます。
このとき、クライアントのコードも生成されます。

$ amplify mock api
# 省略
Running GraphQL codegen
? Choose the code generation language target typescript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.ts
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src/API.ts
? Do you want to generate code for your newly created GraphQL API Yes
# 省略
AppSync Mock endpoint is running at http://xxx.xxx.xxx.xxx:20002

表示されたURLにアクセスすると、Graphiqlの画面が表示され、GraphQLのリクエストを試すことができます。

image.png

特に問題がなければ、amplify push しておきましょう。

❯ amplify push
✔ Successfully pulled backend environment production from the cloud.

Current Environment: production

| Category | Resource name        | Operation | Provider plugin   |
| -------- | -------------------- | --------- | ----------------- |
| Api      | bestTodolist         | Create    | awscloudformation |
| Hosting  | amplifyhosting       | No Change |                   |
| Auth     | besttodolistf818478c | No Change | awscloudformation |
? Are you sure you want to continue? Yes
# 省略
? Do you want to update code for your updated GraphQL API Yes
? Do you want to generate GraphQL statements (queries, mutations and subscription) based on your schema types?
This will overwrite your current graphql queries, mutations and subscriptions Yes
# 省略

フロントエンドの実装

npmパッケージのインストール

以下コマンドでインストールします。

$ yarn add @material-ui/core @material-ui/icons aws-amplify @aws-amplify/ui-react react-hook-form recoil

Lint/Formatterのignoreを追加

Next.jsがexportしたファイルやAWS Amplifyで自動生成したコードはLint/Formatterの対象外にしておきます。

.eslintignore
**/node_modules/*
**/out/*
**/.next/*
src/aws-exports.js
.prettierignore
node_modules
.next
yarn.lock
package-lock.json
public
out

一覧ページの実装

以下ファイルを所定のディレクトリへコピペしてください。

ページ

ベースとなる _app.tsx_document.tsx も必要です。
_document.tsx は、まだFunction Componentでは書けないようです。

https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/pages/_app.tsx
https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/pages/_document.tsx
https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/pages/index.tsx

コンポーネント

各ページの部品となるコンポーネントを作成します。

https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/src/component/Header.tsx
https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/src/component/Footer.tsx
https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/src/component/Todo.tsx

ストア

正直TodoListのレベルであれば、ストアを使う必要は無いのですが、前々から使ってみたいと思っていたRecoilを使うことにしました。

https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/src/store/todos.ts

テーマ

Material UIのベースとなるテーマを定義します。

https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/src/theme.ts

動作確認

CI/CDが無事に完了し、ページにアクセスするとログイン画面が表示されます。

image.png

Create Account でアカウント作成するとログインができるようになります。
無事、一覧画面が無事表示されました。

image.png

その他の画面の実装

新規登録画面

image.png

次のファイルを pages に追加します。

https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/pages/todos/new.tsx

フォームは編集画面でも使うためコンポーネント化しておきます。

https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/src/component/Form.tsx

詳細画面

image.png

次のファイルを pages に追加します。

https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/pages/todos/%5Bid%5D.tsx

編集画面

image.png

次のファイルを pages に追加します。

https://github.com/yuuu/next-ts-amplify-recoil-todolist/blob/master/pages/todos/%5Bid%5D/edit.tsx

まとめ

手元の環境でアプリを開発して画面を揃えるところまでは順調に進んだのですが、CI/CDでハマりどころが多く苦労しました。
ここまでの環境が揃っていれば、あとはスピーディーに開発していけそうな気がしています。

ぜひ、お試しください。

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

【Nuxt.js+Firebase】ログイン不要!気軽に投稿できるオススメのカレー共有サイトを作りました!

カレーの時代到来

近年、カレーブームがますます加速しているように思われます。

ちょうど今、西武池袋で行われてるカレーイベントでは連日行列が続き、
売り切れ商品も多数あるとのこと!

かくいう私も、カレーフリーク。
カレー専用のInstagramを開設し、食べ歩いたカレーを投稿しています。

今回、静的サイトを作る練習として、Nuxt.jsとFirebaseを使って、カレー屋さんの共有サイトを作ってみました!

食べログやInstagramと違い、アカウントは不要、匿名で気軽に投稿できるのが特徴です♪
下記にリンクを貼っているので、ぜひ投稿してみてください!

カレー共有サイト「Curry Freak」

サイトURL ▶▶▶ https://curryfreak.ml/

「新しいCurryを追加」のページから、投稿画面が確認できます。
Image from Gyazo

入力が完了すると、投稿ボタンが出現!
(アップロードが完了する前に投稿するとエラーになるので)
Image from Gyazo

投稿ボタンを押すと、「Curry一覧」ページに遷移して、
投稿したカレー屋さん情報をご覧いただけます♪
Image from Gyazo

作り方

まずNuxt.jsでプロジェクトを作成(参考記事
UIフレームワークを選べるので、今回Buefyを選択しました。

yarn add firebaseでFirebaseのライブラリを追加

yarn dev で動作確認しながら、ページやコンポーネントを作成

UIをちょこっと修正

yarn generateを実行し、distフォルダをNetlifyでデプロイ

独自ドメインを設定して、完成♪(参考記事

ページなどのソースコードはgithubにあげたのでご覧ください!

ソースコード ▶▶▶ https://github.com/twtjudy1128/CurryFreak

つまずいたところ

Firebaseの呼び出し

序盤で「Firebase App named '[DEFAULT]' already exists」というエラーが出て進まず。
Image from Gyazo

こちらの記事を拝見し、
何度もFirebaseを初期化して呼び出してしまっていることが原因だとわかったので、
以下のように初期化のコードを修正したら、すぐエラーが消えました♪

            // Initialize Firebase
            if (firebase.apps.length === 0) {
            firebase.initializeApp(config);
            }

V-modelの使い方

v-onとv-bindをまとめて書くことができるv-model。
フォームで色んな人が使ってるのを見て、私も投稿画面で使ってみました。

     <div class="postform">
        <div>
          <input v-model="title" placeholder="店名"><br>
          <input v-model="name" placeholder="名前"><br>
          <input v-model="memo" placeholder="ひとこと"><br>
          <input v-show="!image_url" type="file" id="image_file" @change="onFileChange"  accept="image/*" required/>
          <b-button type="is-warning" v-on:click='post' v-show="show"><b>投稿</b></b-button>
        </div>
      </div>

ところが、以下のように真っ赤になっちゃいました><

Image from Gyazo

調べると、対応するデータが定義されていなかったので、script部分で、以下のように定義。

data(){
    return {
      image_url: null,
      title:'',
      name:'',
      memo:'',
      downloadURL:'',
      show: false,
    };

すると今度は、以下のエラー

The “data” option should be a function that returns a per-instance value in component definitions.

子コンポーネントでは、dataをオブジェクトではなく、関数として定義する必要があるとのこと・・・(難しい)
というわけで、以下のようにちょこっと変更しただけでエラーが綺麗に消えました!

data:function(){
    return {
      image_url: null,
      title:'',
      name:'',
      memo:'',
      downloadURL:'',
      show: false,
    };

【参考記事】
Vue.jsのv-modelを正しく使う
【Vue.js】The “data” option should be a function that returns a per-instance value in component definitions.というエラーについて

画像とテキストを一緒に投稿する

1番苦戦したところです。笑

Image from Gyazo

上記のような構造を目指して、あーだこーだやってみたのですがエラー続きで心折れかけました。笑

その時に、GyazoやimgurのAPIを使う方法を見つけたのでトライしてみることに。
ところが、GyazoAPIはアクセス権限で引っかかり使えず…。

imgurで試してみたのですが、下記のようなエラーが出ました…(今だ解決しておらず)
Image from Gyazo

かなり苦戦していたのですが、 @tkyko13 さんにご協力いただき、
本来やりたかったFirebase StorageとCloud Firestoreを使った方法でうまく投稿できるようになりました。(大感謝)

コード長くなるので、ソースコードのpost.vueをご覧いただけると幸いです!
ソースコード ▶▶▶ https://github.com/twtjudy1128/CurryFreak

心残りなPOINT

・投稿日時も入れればよかった
・投稿した順に表示できるようにしたい
・画像をアップロードしている間、「アップロード中」のクルクルみたいのを出したい
・UIをもっと綺麗にしたい(フレームワークは便利だけどカスタムのコツがまだ掴めてない)
・ロゴを作りたい
・もう1つページを増やして、おふざけ要素作りたかった

色々やりたいこと挙げるとキリがないですね・・・
でも、手こずりながらも、また1つアウトプットできたことが嬉しいです。

あなたのオススメのカレー屋さんを教えてください♪

最後までご覧いただき、ありがとうございます!
匿名で簡単に投稿できるので、是非あなたのオススメカレーを投稿してみてくださいね!

カレー共有サイト「Curry Freak」 ▶▶▶ https://curryfreak.ml/

みんなでカレー食べて、免疫つけて、今日も1日がんばりまっしょー!!!!!

(*^^)v「よろしければLGTMも宜しくお願いします!」

 

<9/10 18:28追記>
もうこんなに投稿集まってきました~ありがとうございます!
こういう機能も欲しいなどフィードバックもいただけて嬉しいです!
バシバシ投稿よろしくお願いします!
Image from Gyazo

※私の独断で不適切だと思った画像は随時削除しております。ご了承ください。

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

え!? わずか3分でローカルにTypeScriptの実行環境を!?

できらぁ!(様式美)

ということでローカルにTypeScriptの実行環境を作ります。すぐできます。

TypeScriptを使うだけなら、TypeScript playground等を使えばいいと思うのですが、「○○のパッケージを試したい。ついでだからTypeScriptも使いたい」という欲張りさんはローカルに環境構築したくなることもあるでしょう。え? codesandbox? 知らんなぁ。

とにかくローカルにTypeScriptの実行環境を作っていきます。ゴールは「コンパイルして出来たjsファイルをnodeコマンドで実行するところ」までです。

事前準備

以下は事前に準備できてるとします。出来てない方は適当にググってください。

  • node, npm(yarn)

ローカルにTypeScriptの実行環境を作成

ここから本題です。
あと私はyarn派なのでyarnを使います。

プロジェクトの作成

プロジェクトディレクトリを作成して、package.jsonを作りましょう。

$ mkdir typescript_try
$ cd typescript_try/
$ yarn init

yarn init後に色々聞かれますが、とりあえず全部Enterで良いです。(ちゃんと設定したい人はしてください)

TypeScriptのインストール

$ yarn add -D typescript @types/node

typescript等はもちろん開発でしか使わないので、-Dはつけましょう(すぐ忘れる)

tsconfig.json

これが無いとコンパイル出来ないので作ります。ルートディレクトリに置いて下さい。

$ touch tsconfig.json
tsconfig.json
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "module": "CommonJS",
    "outDir": "dist", // コンパイル後に生成されるJSファイルの置き場所をTSCに指示
    "sourceMap": true,
    "strict": true,
    "target": "ES2015"
  },
  "include": ["src"] // TSCがTypeScriptファイルを見つけるためにどのディレクトリを探せば良いか?の指定
}

なおこの内容は、オライリーのTypeScript本の2.3.1 tsconfig.json に記載された内容をベースに作成しました。(yarn tsc --initでも内容は異なりますが最低限のものを作れます。こっちの方が普通かも?)

詳細は割愛しますが、以降の説明に関連する2つのパラメータに関してはjsonのコメントに説明を記載しました。なおTSCはTypeScriptのコンパイラのことです。

他の項目に関してはググってください。もしくはオライリー本を買って下さい! 超良書です。

またこの時点では、"src"/"dist" フォルダが無いため、エディタによってはエラーが表示されるかもしれませんが、そこは一旦スルーして下さい。

TSファイルの作成

実行したいTypeScriptファイルを作成します。
tsconfig.jsonのincludeで指定した通り、srcディレクトリを作成して、その下に作って下さい。

$ mkdir src
$ touch src/index.ts
src/index.ts
const hello: string = 'Hello TypeScript!'
console.log(hello)

コンパイル後に生成されるJSファイルの置き場所を作成

ディレクトリを作るだけでOKです。

$ mkdir dist

もし先ほどtsconfig.jsonでエラーが出ていた場合は、この後エラーが消えていることを確認して下さい。

TSファイルのコンパイル

作成したTypeScriptファイルをコンパイルします。

$ yarn tsc
yarn run v1.22.4
$ node_modules/.bin/tsc
✨  Done in 1.39s.

生成されたJSファイルはdist下に保存されているはずです。確認してみましょう。

dist/index.js
"use strict";
const hello = 'Hello TypeScript!';
console.log(hello);
//# sourceMappingURL=index.js.map

それっぽく出来てますね!

生成されたJSファイルの実行

nodeコマンドで生成されたJSファイルを実行します。

$ node dist/index.js
Hello TypeScript!

これでローカル環境でTypeScriptのコードを実行できました。簡単でしたね!

さいごに

「手元でJavaScript周りのパッケージの動作確認等をする時、サラッとTypeScript使えてたらカッコよくない?」という不純な気持ちで書きました。

ただパッケージによっては、別途TypeScript用の設定が必要だったりするのでそこはご注意くださいm(_ _)m

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

DOMとは

プログラミング勉強日記

2020年9月9日
JSについて調べていくうちにDOMを知り、JSを扱う上で絶対に知らないといけない仕組みだと思ったのでまとめる。

DOMとは

 Document Object Modelの略で「ドム」と呼ばれ、HTMLやXML文書を取り扱うためのAPI。つまり、プログラムからHTMLやXMLを自由に操作するための仕組み。(APIについてはこちらでまとめている)
 DOMではHTMLやXML文書をノードと呼ばれる階層的な構造として識別し、JSなどの様々なプログラミング言語やスクリプトから扱いたいノードを特定して操作できるようにする仕組みを提供する。

特徴

DOMに以下の3つの特徴がある。

  • ツリー構造と呼ばれる階層構造を持つ
  • それぞれノードで説明される
  • WEBページとJSなどのプログラミング言語をつなぐ

階層構造をとる

 階層構造は組織図的なもの。
 HTMLの階層構造は以下のようになる。以下の例では<body>を頂点として下にいくつかの<section><p>で構成されている。これはHTMLで階層構造を構築した場合の一例であり、この階層構造を定義しているものがDOMと呼ばれる仕組みを使っている。

0909.png

各要素はノードで表現される

 DOMではノードと呼ばれる用語が出てくる。
 以下のようにノードは各要素自体のことを表す。特定のノードを基準としたときに、その上にあるノードを親ノード、その下にあるノードを子ノード、同じ階層にあるノードを兄弟姉妹ノードと表現する。
0909-1.png

参考文献

DOMとは
JavaScript初心者でもすぐわかる!DOMとは何か?

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

【Nuxt.js + Firebase】写真を投稿するとAIがハッピー指数を測定してランキング付けするシステムを作ったよ

作ったもの

タイトルの通り、写真を投稿すると、どれくらいハッピーなのかAIがハッピー指数を算出します。
スクリーンショット 2020-09-09 22.48.59.png
写真はパクタソさんより

※「ハッピー指数」としましたが、これって「指数」って言ってもいいものだろうか不安になりましたが、頭の悪そうな語感が気に入ったのでこのまま「ハッピー指数」とします。

そして、同時にハッピー指数のランキングを表示します。

スクリーンショット 2020-09-09 22.57.11.png
写真はパクタソさんより

おお、眩しすぎる。

という訳で、
あなたが世界でどれくらい幸せなのか、調べてみましょう。

公開URL

ウェブアプリとして公開していますので、是非お試してください。

URL:
http://happy-ranking.tk/

ちなみにこのドメインはこちらで取得しました。
freenom

環境

フレームワーク :Nuxt.js
CSSフレームワーク:BULMA
開発プラットフォーム:Firebase

技術面

表情解析

アップロードした写真を、face-api.js というTensorFlow.jsで学習済みの顔認識の機械学習モデルのライブラリに投げて、表情からハッピー度を取得します。
ハッピー度は0から1の間で、大きいほどハッピー度が高いです。

faceapi.resizeResults(detections, displaySize)[0].expressions.happy =
0.999991774559021

ハッピー度はこんな感じで取得されます。
小数点が15桁くらいあるので、1000倍して見やすくしました。

(ご参考)自分の過去記事
【忙しい現代人のために】表情で扇風機を操作するシステムを作ったよ

写真およびデータ保存

  • FirebaseのCloud FireStoreというストレージに写真を保存 storage.png

ここでハマったことを別記事に書きました。
【Firebase + Nuxt.js】FirebaseStorageへの画像アップロードでハマったところ

  • あとで呼び出すために、写真を保存した場所のURLを、FirebaseのFirestoreに保存 (下記画像の picURLフィールド) firestore.png

ハマったところ

Nuxt.js、Vue.js

リストの中でのイメージの表示方法

こちらを見て解決しました。ありがとうございました。
【Vue.js】imgタグのsrc要素は指定の仕方によって読み込み方が違う

たった、これだけなんですけど、凄くハマりました。

<img :src="data.picurl"/>

非同期処理のコードの書き方

以下のようなコードのことです。
毎回、頭がおかしくなりそうです。
=> を多用すると、訳わからなくなります。
じっくりと勉強しなければ。

Pic.vue
methods: {
  post(pic){
                〜中略〜        
                // 画像をStorageにアップロード
                storageRef.put(file).then(() => {   
                    let debug_document =  document.getElementById("happyScore");
                    debug_document.innerHTML = "しばらくお待ちください";

                    // アップロードした画像のURLを取得 
                    const picurl = firebase.storage().ref(fileName).getDownloadURL().then((url) => {    
                        const happyScoreGet = getFaceData( document.getElementById('attachedFile')).then((happyScore) => {
                            let debug_document =  document.getElementById("happyScore");
                            let realhappyScore = happyScore;
                            happyScore = Math.floor(happyScore * 1000); //1000倍
                            debug_document.innerHTML = "ハッピー指数: "+String(happyScore)+"点";
                            //firestoreにデータを保存
                            const setScore = docRef.set({
                                name: this.name,
                                happyScore: happyScore,
                                realhappyScore: realhappyScore ,
                                fileName: fileName ,
                                picurl: url
                            });
                    //ランキング作成へ
                    this.get();
                    });

                }).catch((error) => {
                    console.log(error)
                })
            })
        }
}



face-api.js のファイルのままデプロイすると怒られた

[BABEL] Note: The code generator has deoptimised the styling of /pages/face-api.js as it exceeds the max of 500KB.

500KB以上はダメらしいので、
https://github.com/justadudewhohacks/face-api.js/
に書いてある通り、npmでインストールしました。

npm i face-api.js



face-api.jsの機械学習モデルの場所はstaticフォルダへ

同じディレクトリに保存してはいけません。
デプロイされません。





ハマったところは、その他、たくさんあったけど、書ききれないです。




やりたかったけど出来なかったこと

・送信ボタンを付けたかった
(添付ファイルを選んだ瞬間に送信される)
・画像サイズ制限
・画像をリサイズして保存する
・ハッピー指数がでた瞬間に順位が出るように

コード

index.vue
<template>
  <div class="container">
    <div>
      <br><br>
      <h1 class="title">
        ハッピー・ランキング
      </h1>
      写真を投稿するとハッピー指数を判定して、ランキングします。<br>
      ハッピー指数は1000点が最高得点です。<br><br>
      ※投稿写真は作者が管理しているクラウドサーバーに保存されますので、ご注意ください。
      <br><hr><br><br>
      <client-only placeholder="Loading...">
        <Pic />
      </client-only>
    </div>
  </div>
</template>

<script defer src="face-api.js"></script>
<script defer src="scripts.js"></script>

<script>
import Pic from '~/components/Pic.vue'

export default {
  components: {
    Pic
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  background-color: pink;
}

.title {
  font-family:
    'Quicksand',
    'Source Sans Pro',
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    'Helvetica Neue',
    Arial,
    sans-serif;
  display: block;
  font-weight: bold; 
  font-size: 30px;
  color: #35495e;
  letter-spacing: 1px;
}

#happyScore{
  font-size: 30px;
  font-weight: bold; 
}

.list{
padding-top: 50px;
}

</style>

index.vue
<template>
    <div>
        投稿者名:<input v-model="name" placeholder="投稿者名">           
        <br><br>
        <input @change="post" type="file"  data-label="画像の添付">
        <br>
        <img id="attachedFile" width=350 v-show="uploadedImage" :src="uploadedImage"  />

        <div id="happyScore"></div>
        <br><hr>

      <div class="list">
            <h1 class="title">
                ハッピー指数 トップ100
            </h1>
            <br>
            <ul v-for="(data, index) in allData" :key="data.id" class="menu-list" >
                <li>
                順位: {{index + 1}}<br>
                ハッピー指数 :  {{data.happyScore}}<br>
                投稿者名:{{data.name}} <br>
                <img width=350 :src="data.picurl"/>
                </li>
                <br><br>
            </ul>
      </div>
        <br>
    </div>
</template>

<script>
    import firebase from "firebase/app";
    import "firebase/firestore";
    import 'firebase/storage';
    import * as faceapi from 'face-api.js';
    import uuid from 'uuid';

    export default {
        components: {},

        data(){
            return{
                db: {},
                allData: [],
                name: '',
                fileName: '',
                picurl: '',
                uploadedImage: '',
                happyScore: '',
                realhappyScore: '',
                testId: ''
            }
        },

        methods: {
            //初期化、設定 
            //各人の数値を入れること
            init: () => {
                const config = {
                    apiKey: "",
                    authDomain: "",
                    databaseURL: "",
                    projectId: "",
                    storageBucket: "gs://xxxxxx-99999.appspot.com",
                    messagingSenderId: "",
                    appId: "",
                    measurementId: ""
                };

                // Initialize Firebase
                firebase.initializeApp(config);       
            },

            post(pic){
                const file = pic.target.files[0];
                if(!file.type.match('image.*')) {
                    alert("画像ファイルでお願いします");
                return;
                }

                //イメージファイル描画
                let reader = new FileReader();  
                reader.onload = (pic) => {
                    this.uploadedImage = pic.target.result;
                };
                let imagefiles = pic.target.files || pic.dataTransfer.files;
                reader.readAsDataURL(imagefiles[0]);
                let attachedFile = document.getElementById('attachedFile');           
                const testId = firebase.firestore().collection('pics').doc().id; //ユニークなIDを生成
                const docRef = firebase.firestore().collection('pics').doc(testId);
                const fileName = uuid(); //ファイル名は他と被らないように uuid ライブラリを使って動的に生成
                const storageRef = firebase.storage().ref(fileName);          

                // 画像をStorageにアップロード
                storageRef.put(file).then(() => {   
                    let debug_document =  document.getElementById("happyScore");
                    debug_document.innerHTML = "しばらくお待ちください";

                    // アップロードした画像のURLを取得 
                    const picurl = firebase.storage().ref(fileName).getDownloadURL().then((url) => {    
                        const happyScoreGet = getFaceData( document.getElementById('attachedFile')).then((happyScore) => {
                            let debug_document =  document.getElementById("happyScore");
                            let realhappyScore = happyScore;
                            happyScore = Math.floor(happyScore * 1000); //1000倍
                            debug_document.innerHTML = "ハッピー指数: "+String(happyScore)+"";
                            //firestoreにデータを保存
                            const setScore = docRef.set({
                                name: this.name,
                                happyScore: happyScore,
                                realhappyScore: realhappyScore ,
                                fileName: fileName ,
                                picurl: url
                            });
                    //ランキング作成へ
                    this.get();
                    });

                }).catch((error) => {
                    console.log(error)
                })
            })
        },

        //データ取得
        get: function(){
            this.allData = [];
            //スコアの降順に100個取得    
            firebase.firestore().collection('pics').orderBy('realhappyScore', 'desc').limit(100).get().then(snapshot => {
                snapshot.forEach(doc => {              
                    this.allData.push(doc.data());
                })
            });    
        }
        },  
        mounted(){
        //ページ読み込み時に実行される
        this.init();
        },
    }

    //表情取得
    async function getFaceData(img) { 
        await faceapi.nets.tinyFaceDetector.load("/models") ;//モデル読み込み
        await faceapi.nets.faceLandmark68Net.load("/models") ;//モデル読み込み
        await faceapi.nets.faceRecognitionNet.load("/models") ;//モデル読み込み
        await faceapi.nets.faceExpressionNet.load("/models") ;//モデル読み込み

        const detectionsWithLandmarks = await faceapi.detectAllFaces(img,new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks();
        if (detectionsWithLandmarks.length == 0){
            alert('人間じゃないよ');
        return(0)
        }else{
            const displaySize = { width: attachedFile.width, height: attachedFile.height }
            //1つの顔だけなのでfaceapi.detectAllFacesではなくて detectSingleFaceでよいはずが、本件はdetectAllFacesを使った。
            const detections = await faceapi.detectAllFaces(attachedFile , new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceExpressions()
            const resizedDetections = faceapi.resizeResults(detections, displaySize)
            return(resizedDetections[0].expressions.happy); //ハッピー指数を返す
        }
    }

</script> 












番外編 (うちわ受け)

現在、一緒にProtoOut Studioで学んでる受講生が今までQiitaで取り上げた人物で試してみました。

Juri Tawaraさん
代表作:ジェイソン・ステイサムで妄想するのが日課になっていたので、いっそBOTにしてみた。
ステイサム.png

[UhRhythm](https://qiita.com/UhRhythm)さん
【Vue.js】さ迷うハロオタがお誕生日カレンダーを作った
スクリーンショット 2020-09-09 22.44.53.png

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

【Nuxt.js + Firebase】写真を投稿するとAIがハッピー指数を測定してランキング付けするリア充向けのシステムを作ったよ

作ったもの

タイトルの通り、写真を投稿すると、どれくらいハッピーなのかAIがハッピー指数を算出します。
スクリーンショット 2020-09-09 22.48.59.png
写真はパクタソさんより

※「ハッピー指数」としましたが、これって「指数」って言ってもいいものだろうか不安になりましたが、頭の悪そうな語感が気に入ったのでこのまま「ハッピー指数」とします。



そして、同時にハッピー指数のランキングを表示します。

スクリーンショット 2020-09-09 22.57.11.png
写真はパクタソさんより

おお、眩しすぎる。
キラキラする。
まさにリア充向けシステムだ。

という訳で、
あなたが世界でどれくらい幸せなのか、調べてみましょう。

公開URL

ウェブアプリとして公開していますので、是非お試してください。

URL:
http://happy-ranking.tk/

ちなみにこのドメインはこちらで取得しました。
freenom

環境

フレームワーク :Nuxt.js
CSSフレームワーク:BULMA
開発プラットフォーム:Firebase

技術面

表情解析

アップロードした写真を、face-api.js というTensorFlow.jsで学習済みの顔認識の機械学習モデルのライブラリに投げて、表情からハッピー度を取得します。
ハッピー度は0から1の間で、大きいほどハッピー度が高いです。

faceapi.resizeResults(detections, displaySize)[0].expressions.happy =
0.999991774559021

ハッピー度はこんな感じで取得されます。
小数点が15桁くらいあるので、1000倍して見やすくしました。

(ご参考)自分の過去記事
【忙しい現代人のために】表情で扇風機を操作するシステムを作ったよ

写真およびデータ保存

  • FirebaseのCloud FireStoreというストレージに写真を保存 storage.png

ここでハマったことを別記事に書きました。
【Firebase + Nuxt.js】FirebaseStorageへの画像アップロードでハマったところ

  • あとで呼び出すために、写真を保存した場所のURLを、FirebaseのFirestoreに保存 (下記画像の picURLフィールド) firestore.png

ハマったところ

Nuxt.js、Vue.js

リストの中でのイメージの表示方法

こちらを見て解決しました。ありがとうございました。
【Vue.js】imgタグのsrc要素は指定の仕方によって読み込み方が違う

たった、これだけなんですけど、凄くハマりました。

<img :src="data.picurl"/>

非同期処理のコードの書き方

以下のコードのような非同期処理が苦手です。
then(() =>{ とか、Promiseとか ansy async とか await とか・・・
毎回、頭が混乱してしまいます。



じっくりと勉強しなければ。



face-api.js のファイルのままデプロイすると怒られた

[BABEL] Note: The code generator has deoptimised the styling of /pages/face-api.js as it exceeds the max of 500KB.

500KB以上はダメらしいので、
https://github.com/justadudewhohacks/face-api.js/
に書いてある通り、npmでインストールしました。

npm i face-api.js



face-api.jsの機械学習モデルの場所はstaticフォルダへ

同じディレクトリに保存してはいけません。
デプロイされません。





ハマったところは、その他、たくさんあったけど、書ききれないです。




やりたかったけど出来なかったこと

・送信ボタンを付けたかった
(今のは、添付ファイルを選んだ瞬間に送信される)
・画像をリサイズして保存したかった
・ハッピー指数がでた瞬間に順位が出るようにしたかった

コード

index.vue
<template>
  <div class="container">
    <div>
      <br><br>
      <h1 class="title">
        ハッピー・ランキング
      </h1>
      写真を投稿するとハッピー指数を判定して、ランキングします。<br>
      ハッピー指数は1000点が最高得点です。<br><br>
      ※投稿写真は作者が管理しているクラウドサーバーに保存されますので、ご注意ください。
      <br><hr><br><br>
      <client-only placeholder="Loading...">
        <Pic />
      </client-only>
    </div>
  </div>
</template>

<script defer src="face-api.js"></script>
<script defer src="scripts.js"></script>

<script>
import Pic from '~/components/Pic.vue'

export default {
  components: {
    Pic
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  background-color: pink;
}

.title {
  font-family:
    'Quicksand',
    'Source Sans Pro',
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    'Helvetica Neue',
    Arial,
    sans-serif;
  display: block;
  font-weight: bold; 
  font-size: 30px;
  color: #35495e;
  letter-spacing: 1px;
}

#happyScore{
  font-size: 30px;
  font-weight: bold; 
}

.list{
padding-top: 50px;
}

</style>

index.vue
<template>
    <div>
        投稿者名:<input v-model="name" placeholder="投稿者名">           
        <br><br>
        <input @change="post" type="file"  data-label="画像の添付">
        <br>
        <img id="attachedFile" width=350 v-show="uploadedImage" :src="uploadedImage"  />

        <div id="happyScore"></div>
        <br><hr>

      <div class="list">
            <h1 class="title">
                ハッピー指数 トップ100
            </h1>
            <br>
            <ul v-for="(data, index) in allData" :key="data.id" class="menu-list" >
                <li>
                順位: {{index + 1}}<br>
                ハッピー指数 :  {{data.happyScore}}<br>
                投稿者名:{{data.name}} <br>
                <img width=350 :src="data.picurl"/>
                </li>
                <br><br>
            </ul>
      </div>
        <br>
    </div>
</template>

<script>
    import firebase from "firebase/app";
    import "firebase/firestore";
    import 'firebase/storage';
    import * as faceapi from 'face-api.js';
    import uuid from 'uuid';

    export default {
        components: {},

        data(){
            return{
                db: {},
                allData: [],
                name: '',
                fileName: '',
                picurl: '',
                uploadedImage: '',
                happyScore: '',
                realhappyScore: '',
                testId: ''
            }
        },

        methods: {
            //初期化、設定 
            //各人の数値を入れること
            init: () => {
                const config = {
                    apiKey: "",
                    authDomain: "",
                    databaseURL: "",
                    projectId: "",
                    storageBucket: "gs://xxxxxx-99999.appspot.com",
                    messagingSenderId: "",
                    appId: "",
                    measurementId: ""
                };

                // Initialize Firebase
                firebase.initializeApp(config);       
            },

            post(pic){
                const file = pic.target.files[0];
                if(!file.type.match('image.*')) {
                    alert("画像ファイルでお願いします");
                return;
                }

                //イメージファイル描画
                let reader = new FileReader();  
                reader.onload = (pic) => {
                    this.uploadedImage = pic.target.result;
                };
                let imagefiles = pic.target.files || pic.dataTransfer.files;
                reader.readAsDataURL(imagefiles[0]);
                let attachedFile = document.getElementById('attachedFile');           
                const testId = firebase.firestore().collection('pics').doc().id; //ユニークなIDを生成
                const docRef = firebase.firestore().collection('pics').doc(testId);
                const fileName = uuid(); //ファイル名は他と被らないように uuid ライブラリを使って動的に生成
                const storageRef = firebase.storage().ref(fileName);          

                // 画像をStorageにアップロード
                storageRef.put(file).then(() => {   
                    let debug_document =  document.getElementById("happyScore");
                    debug_document.innerHTML = "しばらくお待ちください";

                    // アップロードした画像のURLを取得 
                    const picurl = firebase.storage().ref(fileName).getDownloadURL().then((url) => {    
                        const happyScoreGet = getFaceData( document.getElementById('attachedFile')).then((happyScore) => {
                            let debug_document =  document.getElementById("happyScore");
                            let realhappyScore = happyScore;
                            happyScore = Math.floor(happyScore * 1000); //1000倍
                            debug_document.innerHTML = "ハッピー指数: "+String(happyScore)+"";
                            //firestoreにデータを保存
                            const setScore = docRef.set({
                                name: this.name,
                                happyScore: happyScore,
                                realhappyScore: realhappyScore ,
                                fileName: fileName ,
                                picurl: url
                            });
                    //ランキング作成へ
                    this.get();
                    });

                }).catch((error) => {
                    console.log(error)
                })
            })
        },

        //データ取得
        get: function(){
            this.allData = [];
            //スコアの降順に100個取得    
            firebase.firestore().collection('pics').orderBy('realhappyScore', 'desc').limit(100).get().then(snapshot => {
                snapshot.forEach(doc => {              
                    this.allData.push(doc.data());
                })
            });    
        }
        },  
        mounted(){
        //ページ読み込み時に実行される
        this.init();
        },
    }

    //表情取得
    async function getFaceData(img) { 
        await faceapi.nets.tinyFaceDetector.load("/models") ;//モデル読み込み
        await faceapi.nets.faceLandmark68Net.load("/models") ;//モデル読み込み
        await faceapi.nets.faceRecognitionNet.load("/models") ;//モデル読み込み
        await faceapi.nets.faceExpressionNet.load("/models") ;//モデル読み込み

        const detectionsWithLandmarks = await faceapi.detectAllFaces(img,new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks();
        if (detectionsWithLandmarks.length == 0){
            alert('人間じゃないよ');
        return(0)
        }else{
            const displaySize = { width: attachedFile.width, height: attachedFile.height }
            //1つの顔だけなのでfaceapi.detectAllFacesではなくて detectSingleFaceでよいはずが、本件はdetectAllFacesを使った。
            const detections = await faceapi.detectAllFaces(attachedFile , new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceExpressions()
            const resizedDetections = faceapi.resizeResults(detections, displaySize)
            return(resizedDetections[0].expressions.happy); //ハッピー指数を返す
        }
    }

</script> 






























番外編 (内輪ネタ)

現在、一緒にProtoOut Studioで学んでる受講生が今までQiitaで取り上げた人物で試してみました。

Juri Tawaraさん
代表作:ジェイソン・ステイサムで妄想するのが日課になっていたので、いっそBOTにしてみた。
ステイサム.png

UhRhythmさん
【Vue.js】さ迷うハロオタがお誕生日カレンダーを作った
スクリーンショット 2020-09-09 22.44.53.png

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

みんなで物語をつくりながら連想力を鍛えてアイデア発想力を磨こう

アイデア発想力とは?

アイデアとは、情報と情報の掛け合わせであると言われています。
その掛け合わせるために必要な力が、連想力です。

例えば、情報Aと情報Bの関連性を発見したり、情報Aと情報Bをベースにホップ・ステップ・ジャンプで飛躍してみたりするような連想によって、新しいアイデアを生み出していきます。

なので、アイデアを生み出す力を磨くには、連想力を磨くことが大事です。

じゃあ、どうやると磨いていけるのか?

言葉遊びゲーム「空文字アワー」とは?

その1つの方法が、言葉遊びゲーム「空文字アワー」です。

このゲームは、ある一文から連想した情報を文に追加することで、
新たな物語をつくっていきます。

まあやってみるとわかると思うので、
詳細はこちらをご覧いただき、
ぜひ最新の文に情報を追加してみてください。

使った技術

・nuxt.js
・Vue.js
・FirebaseのCloud Firestore

実装の仕方

今回は、初めてnuxt.jsってやつを使いました。
正直使いこなすには程遠いですが、それでも、nuxt.js、Vue.js、Cloud Firestoreを使い、独自ドメインでNetlifyで公開まで持っていけたのは進歩です。

nuxt.jsの流れ

yarn create nuxt-app [任意のプロジェクト名]

今回のUI frameworkは、

UI framework: Bulma

を使用。

ひとまず形ができたら、

$ yarn dev

をして、ローカルサーバーからサイトが閲覧できるようにしながら、
pagesやComponentsの中のファイルを作りました。

で、それができたら、

$ yarn generate

する。

これによって、distファイルができるので、そのファイルごとNetlifyに取り込むと、すぐにWEBアプリが公開できました。

独自ドメインでの公開の流れ

まずは無料で独自ドメインを取得します。色々無料で取得する方法があるようですが、今回はfreenomで取得しました。

image10.png

freenomで取得する際の流れはこちらを参照しました。

ドメインを取得できたら、My domainのURLや情報をNetlifyの方に入力すると、少し時間はかかかるものの、わあしはすぐににWEBが独自ドメインで公開できます。

nuxt.jsのpages

index.vue
<template>
  <div class="container">
    <div>
      <Logo />
      <h1 class="title">
        【言葉ゲーム】空文字アワー
        <p>~ないものつなぎ~</p>
      </h1>
      <div class = "content-explain">
        <p>ある簡単な文に( )があります。ここに「言葉」を入れ、さらに( )を加えます。
          次の人も同じことをする。これを全員で繰り返していくゲームです。
          「空文字アワー」は、どんどんつないで変化を起こすのが醍醐味です。
          最終的にどんなストーリーができあがるかは、みなさんのセンス次第。</p>
      </div>
      <div class = "content-explain">
        <h2>進め方</h2>
        <ol>
          <li>最初の一文と( )を提示します。</li>
          <li>次に答える人が( )に言葉を加え、自分の言葉を加えた( )を外します。</li>
          <li>さらに( )を好きな位置に加えます。これを期間内繰り返します。</li> 
        </ol> 
      </div>

      <div class = "content-explain2">
        <h2>ルール</h2>
        <ol>
          <li>連続して同じ人が答えることはできません。</li>
          <li>連続しない限り、期間中何度でも回答できます。</li>
          <li>手前の全ての回答をコピペして残してください。</li> 
          <li>元の文を修正することはできません。</li>
          <li>文意が伝わるように展開していきましょう。</li> 
        </ol> 
      </div>

      <div class = "content-explain2">
        <h2>例はこちら</h2>
        <p>【Hiro】東京では( )雨が降っていた。</p>
        <p>【お名前】( )東京では季節はずれの雨が降っていた。</p>
        <p>【お名前】高層ビルの立ち並ぶ東京では季節はずれの雨が( )降っていた。</p>
        <p>【お名前】高層ビルの立ち並ぶ東京では季節はずれの雨がしとしとと降っていた( )。</p>
        <p>【お名前】高層ビルの立ち並ぶ東京で( )は季節はずれの雨がしとしとと降っていたのを見て昨年のことを思い出した。</p> 
      </div>

      <div class = "content-explain2">
        <h2>今回のテーマ:桃太郎</h2>
        <img src = "../image01.jpg">
        <client-only placeholder="Loading...">
              <Memo />
        </client-only> 
      </div>
      <div class = "footer">

      </div>

    </div>
  </div>
</template>

<script>
export default {}
</script>

<style>
h2{
  font-size:30px;
}

.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.content-explain{
  margin: 40 auto;
  height: 150px;
}

.content-explain2{
  margin: 40 auto;
  height: 200px;
}

.title {
  font-family:
    'Quicksand',
    'Source Sans Pro',
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    'Helvetica Neue',
    Arial,
    sans-serif;
  display: block;
  font-weight: 300;
  font-size: 50px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}

.footer{
  height: 200px;
}
</style>

memo.vue
<template>
    <div>
        ★★★つくられた物語★★★
        <!-- {{allData}} -->
        <ul v-for="data in allData" :key="data.id" class="menu-list" >
            <li>
                {{data.name}} / {{data.answer}}
            </li>
        </ul>

         <p>
            <input v-model="name" placeholder="名前">
            <input v-model="answer" placeholder="回答">
            <button v-on:click='post'>送信</button>
        </p>
    </div>
</template>

<script>
    import firebase from "firebase/app";
    import "firebase/firestore";

    export default {
        components: {},

        data(){
            return{
                db: {},
                allData: [],
                name: '',
                answer: 'ここに回答',
            }
        },

        methods: {
            init: () => {
                const config = {
                    apiKey: "AIzaSyBvouEQqs3Cqz_F-re7SCW-FLvPuISQsnc",
                    authDomain: "protoout-2359e.firebaseapp.com",
                    databaseURL: "https://protoout-2359e.firebaseio.com",
                    projectId: "protoout-2359e",
                    storageBucket: "protoout-2359e.appspot.com",
                    messagingSenderId: "1085072592944",
                    appId: "1:1085072592944:web:30da6171b08c3734979df5",
                    measurementId: "G-T3FMEWMY87"
            };

            // Initialize Firebase
            firebase.initializeApp(config);
            },

            post: function(){
                const testId = firebase.firestore().collection('memos').doc().id; //ユニークなIDを生成
                const docRef = firebase.firestore().collection('memos').doc(testId);
                const setAda = docRef.set({
                name: this.name,
                answer: this.answer
            });
            this.get();
            },

            get: function(){
                this.allData = [];
                firebase.firestore().collection('memos').get().then(snapshot => {
                    snapshot.forEach(doc => {
                    // console.log(doc);
                    this.allData.push(doc.data());
                })
            });

            }

        },

        mounted(){
            this.init();
            this.get();
        },
    }
</script>

<style>

</style>

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

importを使ってみる

環境
初書:2020/09/10

前書き

javascriptにも名前空間というのがないのか調べていたところ、importというのを見つけたので、
名前空間ではないがそれっぽい動作になりそうなので使ってみる。

前提条件

・import / exportはES6以降でしか対応していない。そのため、IE対応のHPを作成しようと考えている場合は使えない。
・import / exportを使用した場合、宣言に関わらずstrict モードになる。
・ローカル環境では動作しない。(httpもしくはhttpsから始まれば使える。Finderなどでhtmlファイルダブルクリックで開くようなものは動かない、らしい)

記述方法

ライブラリ側

ライブラリとして提供したいものにexport関数を付ける

lib.js
export function id(val){
    return document.getElementById(val);
}
export var pi = 3.14;

ちなみにエクスポート可能なものは、var, let, const, function, class
また、他にもexportする方法はいくつかあるので、詳細はMDN export

利用側

使う方は何ていうのが正解なのだろうか。。?
まぁとりあえず、こちらの方は上記のライブラリをインポートする必要がある
今回は上記のlib.jsと、使う側のmain.jsは同じフォルダにあると仮定する。

main.js
import {id, pi} from "./lib.js";
console.log(id("aa").innerHTML);
console.log(pi);

もしくは

main.js
import * as lib from "./lib.js";
console.log(lib.id("aa"));
console.log(lib.pi);

上の場合は、宣言された名前の通りに使用する事が可能かつ必要なもののみインポート可能で、下の場合は、複数のライブラリを読み込む際に名前の競合を避ける事ができる。
ちなみに*は"全ての"という意味。ここを{id, pi}に置き換えることは出来ない。
こちらも他にimportする方法はいくつかあるので、詳細はMDN import

使ってみる

使う際は、main.jsのみ読み込めばいい。またtype="module"を付ける必要がある。
で、このmoduleというのが普通のjavascriptと少し異なる箇所になる。

詳細は、JavaScript モジュールを読んでいただくとして、普通のscriptを読み込むのと異なり、
・ローカルで実行出来ない(前提条件に記述したもの)
・自動的にstrictモードになる(前提条件に記述したもの)
・defer属性を付けなくても遅延実行される
グローバル環境からアクセス出来ない
という制約が出来ます。1

ということでhtmlの記述

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
</head>
<body>
<div id="aa">aaaaaa</div>
<script type="module" src="main.js"></script>
</body>
</html>

これで、コンソール上には<div id="aa">aaaaaa</div>3.14が表示されているはず。

注意点

"使ってみる"の箇所の太字欄にも記載してある通り、グローバル環境からアクセス出来なくなる。
そのため、例えばデバッグのためにコンソールから変数にアクセスしようとしても失敗する。
また、main.jsmain2.jsのように複数読み込んでも、import/exportの関係では無い場合はお互いの変数や関数にアクセスすることは出来ない。
逆にいうと、ファイルの全体を(() => {})()で囲わなくてもグローバル変数を荒らさない。
また、main.js内からグローバル変数にアクセスすることは可能。

参考サイト

JavaScriptのimport文を使ってみた
JavaScript 別ファイルからクラスをインポートしたい
import
export

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