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

転生したらGoogleボットだった件

※フィクションです。Googleの検索アルゴリズムが実際このように動いているという話ではありません。

やれやれ、ワイは蘇生した。

ワイ「・・・うーん・・・ハッ!?」
ワイ「あれ・・・?」
ワイ「ワイ・・・生きてるんか!?」
ワイ「・・・何やこの真っ白い部屋は・・・!」
ワイ「何がどうなってるんや・・・」
ワイ「確かワイは・・・」

それは昨日のこと

ワイ「確か、ワイらがようやくWebサイトを作り上げた後に」
ワイ「クライアントの一番偉い人が登場して」
ワイ「仕様を根本から変えたいって言い出したんや・・・」
ワイ「それまでのウォーターフォール型開発から、突如メテオフォール型開発1に方針転換したことで」
ワイ「確か、ワイはショックで殉職してしもうたはず・・・」
ワイ「・・・と思ってたけど、どうやら一命を取り留めたようやな・・・」

しかし何かが違う

ワイ「でも、何か普段と違う感じがするで・・・」
ワイ「・・・あっ!!!」
ワイ「何やこの体・・・!」
ワイ「ワイ、Googlebotになってもうとるやないかい!!!」

ハリー先輩「なんで体を見て分かんねん
ハリー先輩「設定ムチャクチャやん

ハリー先輩もいた

ワイ「ハリー先輩!?

ハリー先輩「どうやら俺達はメテオフォールのショックで、チームメンバー全員が殉職してもうたみたいや」
ハリー先輩「そしてこの世界に転生したいうことやな」

ワイ「そんな・・・」
ワイ「ところでこの真っ白い部屋は何でっか?」
ワイ「ここはどういう世界なんでっか!?」

ここは検索エンジン

ハリー先輩「どうやらここはGoogleの検索エンジンや」
ハリー先輩「世界中のWebサイトの情報を集めて」
ハリー先輩「ユーザーが検索したワードに関する情報が載っているサイトを」
ハリー先輩「一覧にして提供するのがお仕事や」

ワイ「おお・・・!そうなんでっか・・・!」
ワイ「さすがハリー先輩・・・」
ワイ「この何もない真っ白い部屋の中に急にほっぽり出されて
ワイ「ノーヒントでそこまで状況を把握してまうなんて・・・」
ワイ「やっぱりハンパなさ過ぎますわ・・・!」

ハリー先輩「そんなことはええ」
ハリー先輩「ウダウダ言うててもしゃあない」
ハリー先輩「とにかく、お前はこの世界ではGooglebotや」
ハリー先輩「早くWebサイトをクローリングしてこい!」

ワイ「はいっ!」

クローリング開始

ワイ「まずはhogehoge.comいうサイトやな・・・」
ワイ「おお、一瞬で着いたで」
ワイ「どれどれ・・・」
ワイ「・・・何やこのWebサイト」
ワイ「中身が空っぽやんけ」

<div id="app"></div>

ワイ「body要素の中身、実質これだけやないか」
ワイ「ん・・・?」
ワイ「・・・なんかワイの中から不思議な力を感じるで・・・!」
ワイ「!?

<div id="app">
    <header>
        <h1>bfjnxkrhorzlb</h1>
    </header>
    <nav>
        <ul>
            <li><a href="/aaa/" title="">lksbjhiu</a></li>
            <li><a href="/bbb/" title="">stih;bjb</a></li>
            <li><a href="/ccc/" title="">nbsltuhbkls</a></li>
        </ul>
    </nav>
    <section>
        <h2>sep:igzrjf:</h2>
        <p>;tpeshj;kzjbxkkb</p>
    </section>

    <!-- 省略 -->

    <footer>
        gjs;rsoibjvkjg;lrgk
    </footer>
</div>

ワイ「おお・・・!」
ワイ「さっきまで空っぽに見えたdiv要素の中に」
ワイ「色んな要素たちが生成されたで・・・!」
ワイ「ワイの中のJavaScriptパワーが目覚めて」
ワイ「このサイト内のスクリプトを解釈することができたんやな」
ワイ「前世ではあんなに読めへんかったJavaScriptが」
ワイ「無意識レベルで理解できたで・・・!」
ワイ「最近のGoogleのクローラーは最新のChrome相当やっていうもんな」
ワイ「せやからこういう、JSで動的に生成されるページも解釈可能なんやな」
ワイ「よっしゃ、この調子で色んなサイトを見て回って、内容を保存していくで!」

しばらくして、ハリー先輩のところへ帰る

ワイ「ハリー先輩!」
ワイ「とりあえず10サイトで2,000ページほど保存して持ってきました!」

ハリー先輩「おう、ご苦労やったな」
ハリー先輩「どれどれ、まずはhogehoge.comの内容を見てみよか・・・」

解析開始

ハリー先輩「なるほどな、このページの・・・」
ハリー先輩「ここからここまでの部分は、このテーマについて書いてあるんやな・・・」

ワイ「え?ハリー先輩・・・」

<section>
    <h2>sep:igzrjf:</h2>
    <p>;tpeshj;kzjbxkkb</p>
    <!-- 省略 -->
</section>

ワイ「この変な文字列読めるんでっか!?」

ハリー先輩「いや、読めへんで」
ハリー先輩「俺たちロボットは、日本語や英語みたいな自然言語をちゃんと理解できるわけやないからな」
ハリー先輩「sep:igzrjf:みたいな意味不明な文字列としてしか認識できひんねや」

ワイ「ほな何で文書の構造みたいなんが分かるんでっか?」

ハリー先輩「このページはちゃんとセマンティックなタグでマークアップされとるからな」

ワイ「セマンティックなタグ・・・?」

ハリー先輩「html5からぎょうさん増えた、意味のあるタグのことや」
ハリー先輩「例えば・・・」

<div>
    <h2>sep:igzrjf:</h2>
    <p>;tpeshj;kzjbxkkb</p>
    <!-- 省略 -->
</div>

ハリー先輩「こんな感じでsectionやなくてdivタグでマークアップされとったら」
ハリー先輩「その中のh2タグがどの範囲に対する見出しなのかが曖昧やねん」
ハリー先輩「divタグってのは、文書構造的な意味を持たない汎用的なタグやからな」
ハリー先輩「せやけど、このページではちゃんとsectionタグが使われとるから」
ハリー先輩「このh2要素は、この章(セクション)に対する見出しなんやな、って分かんねん」
ハリー先輩「俺たちロボットでも読みやすい、マシンリーダブルなhtml文書やっちゅうことや」

ワイ「おお・・・なるほど・・・」

??「こっちのページなんかも分かりやすいですよ

ワイ「・・・君は・・・!?」

ハスケル子も転生していた

ワイ「天才中学生ハスケル子ちゃんやないか!」
ワイ「君も転生しとったんやな・・・!」
ワイ「っていうか、インターンなのにブラック案件に巻き込んでごめんな・・・」

ハスケル子「そんなことは今どうでもいいんです」

ワイ「(殉職も意に介さず!?)」

ハスケル子「ここのhtmlソースを見てください」

<figure>
    <img src="aaa.png" alt="a;grhb;z">
    <figcaption>bns*?gjrsj</figcaption>
</figure>

ハスケル子「ちゃんとfigureタグが使われているので」
ハスケル子「このfigcaptionが、この画像に対する説明だってことが分かります」

ワイ「おお〜、そんなタグもあんねんな・・・!」
ワイ「たしかに・・・」

<div>
    <img src="aaa.png" alt="a;grhb;z">
    <span>bns*?gjrsj</span>
</div>

ワイ「もし、こんな感じでdivspanばっかりでマークアップされとったら」
ワイ「ワイらロボットには、この画像とテキストの関係性が分からへんもんな」

ハスケル子「そういう事です」

ワイ「なるほどな〜」
ワイ「セマンティック要素を使用すると、検索エンジンに対して文書構造を明確にアピールできる・・・」
ワイ「とか聞いたことあったけど、こういうことやったんやな・・・」
ワイ「ホンマいろいろ知ってんな、ハスケル子ちゃん」

ハスケル子「やめ太郎くん・・・やめ太郎くん・・・

ワイ「ん、ハスケル子ちゃん?」
ワイ「どないしたんや?」

ハスケル子「やめ太郎くん・・・やめ太郎くん・・・」

ワイ「ハリー先輩!ハスケル子ちゃんの様子がおかしいでっせ!」

ハリー先輩「やめ太郎くん・・・やめ太郎くん・・・」

ワイ「ハリー先輩まで!」
ワイ「どないなってんねや・・・!」

ここで目が覚める

社長「おい、やめ太郎くん」
社長「堂々と昼寝すなよ・・・」

ワイ「ファッ!?
ワイ「ワイは殉職してGooglebotに・・・」
ワイ「早くクローリングの続きをせんと・・・」

社長「何を寝ぼけてんねん・・・」
社長「さっさと昨日頼んだhtmlコーディング終わらしてくれや・・・」

ワイ「ゆ、夢やったんか・・・」

気を取り直してマークアップ

ワイ「よっしゃ、夢の中で学んだことを活かして」
ワイ「セマンティックなマークアップをしていくで!」

しかし5分後

ワイ「ここは何のタグでマークアップすべきかな・・・」
ワイ「いうてもワイ、アホやから・・・」
ワイ「そもそも文章内の言葉の意味が分からん・・・」
ワイ「意味の分からん言葉意味のあるタグなんて付けられまっかいな・・・」
ワイ「・・・もう全部divタグでええわ!!!」

社長「おい、さっきから全部聞こえてんで・・・!」
社長「ほんまに殉職させたろかい!!!

〜おしまい〜


  1. 神(偉い人)が現れて、隕石のような破壊力のある指示で現場を無茶苦茶にする開発手法。 

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

Nuxt.js + Vuetify.jsでグローバルなイベントのやり取りを event bus でやってみる

要は Snackbartoasts などのメッセージを $on$emit を使ってグローバルなイベントのやり取りをする感じ。
(これを global event bus というらしい)

npm install

plugins/event-bus.client.js 作成

import Vue from 'vue'

export default (ctx, inject) => {
  inject('bus', new Vue())
}

nuxt.config.js にプラグイン登録

plugins: [
    '~/plugins/event-bus.client.js'
]

Snackbars.vue コンポーネントを作成

<template>
  <v-snackbar
    v-model="isEnabled"
    :color="color"
    :multi-line="true"
    :timeout="timeout"
    top
  >
    {{ text }}
    <v-btn
      dark
      flat
      @click="isEnabled = false"
    >
      <v-icon>
        close
      </v-icon>
    </v-btn>
  </v-snackbar>
</template>

<script>
export default {
  data () {
    return {
      isEnabled: false,
      text: '',
      color: 'success',
      timeout: 5000
    }
  },
  created () {
    this.$bus.$on('snackbar:success', (text) => {
      this.isEnabled = !!text
      this.text = text
    })
    this.$bus.$on('snackbar:error', (text) => {
      this.isEnabled = !!text
      this.text = text
      this.color = 'error'
      this.timeout = 0
    })
  }
}
</script>

layouts/default.vue

<template>
  <v-app>
    <v-content>
      <nuxt />
    </v-content>

    <Snackbars />
  </v-app>
</template>

<script>
import Snackbars from '@/components/common/Snackbars'

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

pages/index.vue

<template>
  <v-btn
    @click="successBtn"
  >
    successBtn
  </v-btn>
  <v-btn
    @click="errorBtn"
  >
    errorBtn
  </v-btn>
</template>

<script>
export default {
  methods: {
    successBtn () {
      this.$bus.$emit('snackbar:success', 'success!!!1')
    }
    errorBtn () {
      this.$bus.$emit('snackbar:error', 'error!!!1')
    }
  }
}
</script>

これで successBtnerrorBtn を押すと Snackbars でメッセージが表示されるはず。

参考リンク

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

ES6まとめ

はじめに

const,let,アロー関数,class構文,分割代入について書きます。

ES6のメリット

ES6を学ぶメリットは従来より簡単で明瞭な構文で記述出来ること

const

const wanko = 'toypoo';
console.log(wanko);

wanko = 'pome'; //error

constは再代入と再宣言が不可能な宣言です。
wankoはtoypooのままなのです。

let

let wanko = 'toypoo';
console.log(wanko);

wanko = 'pome';
console.log(wanko);

let wanko = 'pome';  //error

let,const まとめ

let,const共に再宣言は出来ません。
letだけは再代入が可能となります。constは再代入も再宣言も出来ない頑固なやつです。

letとconstは場合によって使い分けていく必要があります。

アロー関数

//従来の無名関数
var wanko = function(toypoo,pome){
    return toypoo+pome;
};
//こっからはアロー関数
const wanko = (toypoo,pome) => {
    return toypoo + pome;
};

const wanko = (toypoo,pome) => toypoo+pome;

const wanko = (toypoo,pome) => ({ sum:toypoo+pome});

従来のものより簡単に書くことが出来ていますね。
functionがなくなり、returnもなくなり、どんどん見やすくなっていってます。

const Counter = function(){
  this.coiunt = 0;
};

Counter.prototype.increment = function(){
   setTimeout(() => {
      this.count++;
      console.log(this.counte);
   },1000);

const counter = new Counter().increment();

アロー関数は関数が定義されたスコープ内のthisを参照することが出来ます。

グローバル関数を参照していたためにおこるエラーを回避できますし、別の変数にthisを参照させる必要がなくなります。

ここで言うスコープとは作った変数や関数が使える範囲のことです。
グローバル変数のスコープは変数宣言位置以降からファイル終端までです。
グローバル変数とは複数の関数から使用することの出来る変数のことを言います。
かなり強いイメージです。
これに対し一つの関数内のみで使用できる変数のことをローカル変数といいます。
弱い感じしますね><

prototypeはReactでバリデーションする時にも出てきたものですが、prototypeはオブジェクト同士の繋がりを保持し、継承させるために使われています。

class構文

class wanko {
    constructor(name){
        this.name = name;
    }
    sayHello(){
        console.log('Hello,I'm' + this.getName());
    }
    getName(){
        return this.name;
    }
}

class構文はprototypeベースのクラス定義構文の糖衣構文です。
ざっくりいうとクラブ構文を簡単に書けるようになる便利なものってことです。
Javascriptはprototypeベースのプログラミング言語なのでアロー関数のコード内で書いたようにprototypeを使ってオブジェクト同士を繋げるのですが、class構文を使えばいちいちprototypeなんて書かずにclassを定義できます。

分割代入

分割代入は、配列とオブジェクトを分解して、要素とプロパティを一つ一つの変数に
分解するための構文です。

配列の分割代入

let list = [10,20,30,40,50];
let [item0,item1,...other] = list;

alert(other); //[30,40,50]

変数の値を入れかえることも可能です。

オブジェクトの分割代入

let toypoo ={
    name: 'toypo',
    age:2,
    hair: {
       type: 'mokomoko',
       color: ' brown'
    }
}

let {hair,hair:{color}}= toypoo;

console.log(color);

let toypoo ={
    name: 'toypo',
    age:2,
    hair: {
       type: 'mokomoko',
       color: ' brown'
    }
}

let {age: wankoage, name: wankoname} = toypoo;

console.log(wankoname); //toypo

key: newkeyで別の名前の変数に値を割り当てられます。

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

MagicScript Landscape Application. UiColorPicker

Prepare
Magic Leap One
https://www.magicleap.com/magic-leap-one

mlsdk v.0.20.0
https://creator.magicleap.com/downloads/lumin-sdk/overview

magic-script-cli v2.0.1
https://www.npmjs.com/package/magic-script-cli

magic-script-polyfills v2.1.1
https://www.npmjs.com/package/magic-script-polyfills

glb file
https://poly.google.com/view/9z0VORJQF31

Create Project

magic-script init my-color org.magicscript.color "Color Picker"
cd my-color
mkdir res

Put glb file in res folder.

Code
Change app.package

DATAS = "digest.sha512.signed" : "." \
        "bin/" : "bin/" \
        "res/" : "res/"
OPTIONS = package/minApiLevel/2

Change app.js

import { LandscapeApp, ui } from 'lumin';
const { 
    UiText
  , UiColorPicker
  , UiLinearLayout
  , Alignment
} = ui;

export class App extends LandscapeApp {
  onAppStart () {
    const prism = this.requestNewPrism([1.0, 1.0, 1.0]);

    const layout = UiLinearLayout.Create(prism);
    layout.setAlignment(Alignment.CENTER_CENTER);

    const resource = prism.createModelResourceId("res/Buggy.glb", 0.004);
    const model = prism.createModelNode(resource);
    layout.addItem(model, [0.01, 0.01, 0.01, 0.01]);

    const color_picker = UiColorPicker.Create(prism, [1.0, 1.0, 1.0, 1.0], 0.5);
    color_picker.setAlignment(Alignment.CENTER_CENTER);
    layout.addItem(color_picker, [0.01, 0.01, 0.01, 0.01]);
    color_picker.onColorChangedSub(function(event)
    {
      model.setColor(color_picker.getColor());
    });
    prism.getRootNode().addChild(layout);
  }
}

Build

magic-script build -i

Run

magic-script run --port=10000

Reference
UiColorPicker (MagicScript API)
https://docs.magicscript.org/lumin.ui.UiColorPicker.html

magicscript
https://www.magicscript.org/

Thanks!

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

Magic Leap MagicScript Landscape Application. UiColorPicker

Prepare
Magic Leap One
https://www.magicleap.com/magic-leap-one

mlsdk v.0.20.0
https://creator.magicleap.com/downloads/lumin-sdk/overview

magic-script-cli v2.0.1
https://www.npmjs.com/package/magic-script-cli

magic-script-polyfills v2.1.1
https://www.npmjs.com/package/magic-script-polyfills

glb file
https://poly.google.com/view/9z0VORJQF31

Create Project

magic-script init my-color org.magicscript.color "Color Picker"
cd my-color
mkdir res

Put glb file in res folder.

Code
Change app.package

DATAS = "digest.sha512.signed" : "." \
        "bin/" : "bin/" \
        "res/" : "res/"
OPTIONS = package/minApiLevel/2

Change app.js

import { LandscapeApp, ui } from 'lumin';
const { 
    UiText
  , UiColorPicker
  , UiLinearLayout
  , Alignment
} = ui;

export class App extends LandscapeApp {
  onAppStart () {
    const prism = this.requestNewPrism([1.0, 1.0, 1.0]);

    const layout = UiLinearLayout.Create(prism);
    layout.setAlignment(Alignment.CENTER_CENTER);

    const resource = prism.createModelResourceId("res/Buggy.glb", 0.004);
    const model = prism.createModelNode(resource);
    layout.addItem(model, [0.01, 0.01, 0.01, 0.01]);

    const color_picker = UiColorPicker.Create(prism, [1.0, 1.0, 1.0, 1.0], 0.5);
    color_picker.setAlignment(Alignment.CENTER_CENTER);
    layout.addItem(color_picker, [0.01, 0.01, 0.01, 0.01]);
    color_picker.onColorChangedSub(function(event)
    {
      model.setColor(color_picker.getColor());
    });
    prism.getRootNode().addChild(layout);
  }
}

Build

magic-script build -i

Run

magic-script run --port=10000

Reference
UiColorPicker (MagicScript API)
https://docs.magicscript.org/lumin.ui.UiColorPicker.html

magicscript
https://www.magicscript.org/

Thanks!

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

【Vue.js】Web API通信のデザインパターン (個人的ベストプラクティス)

はじめに

Vue.jsを使用したアプリケーションでのWeb API呼び出しのデザインパターンについて調べてみました。
しかし検索して出てくるチュートリアルやサンプルはコンポーネント内でaxiosをインスタンス化していたり、Vuexの中でaxiosを使用するというサンプルが多数見受けられました。しかし実際のプロダクトでこれをしてしまうと

  • コンポーネント内でAPIアクセスの直書きによって単体テストが困難に
  • Vuex(actions)の肥大化(使い回さない処理はStoreに記述しないほうがいいとする文献もある)
  • API通信部分をPureJSでモジュール化しても依存度がイマイチ下がらない(コンポーネントでモジュールをインポートするため)。

などなど問題になることが多そうでした。
ある日、Jorge氏が投稿した「Vue API calls in a smart way」という記事にたどり着きました。
結果から言うとこのデザインパターンは自分にはベストな方法となったため、著者の許可を得て紹介します。以下拙訳(一部意訳)です。

ここから翻訳

長い間、APIを呼び出すための様々な方法を公開したいと思いました。この章では私にとって最適なパターンについて話したいと思います。RepositoryFactoryを紹介させてください。

私はこのアプローチが非常によくスケールでき、ほぼいつも上手くいくため大好きです。

なぜかを説明しましょう:

1つはリポジトリパターンを使用して、データを返す以外のロジックなしで分離された方法でリソースににアクセスできます。
もう1つはファクトリパターンを使用して、各ケースで必要なリポジトリ、または環境のロジックをインスタンス化します。必要に応じて、ファクトリがモックリポジトリと本番リポジトリのどちらをインスタンス化するかを決定できるという利点があります。

各コンポーネント内にaxiosのインスタンスを使用したサンプルをどれだけ見たでしょうか。
私はそれを見るたびに毎回疑問に思います?

  • 呼び出し処理を再利用するときはどうなりますか?
  • エンドポイントが変わったときはどうなりますか?
  • ほかのプロジェクトでAPI呼び出し処理を再利用したい場合はどうなりますか?
  • コードをリファクタしたり、Vuezxのactionsに移動する場合はどうなりますか?
  • 複数のリソースがありますが、4つの異なるエンドポイントを定義する必要がありますか?
  • テストのためにモックなどのエンドポイントをどのように扱うことができますか?

0_5m-dDbSZ3gb8oaCJ.gif
30個のファイルを変えるよりも1個のファイルを変えるほうが簡単でしょう??

正しい方法

このケースではコードをシンプルにして、さまざまなリソースを定義するために、POJO(Plain Old JavaScript Objects)を使用します。(もちろん、必要に応じてクラスを使えます)axiosの設定を定義しましょう。ファイル名はリソースの接続確立する責任があるため、Repository.jsとします。

serviceまたは単にAPIという用語を使う人もいますが、Repositoryがその機能を最もよく定義しているように思えます。
image.png

それから、それぞれのエンティティのリソース定義をする必要があります。
例えばブログがあるとしましょう。postsエンティティがあり、すべてのCRUD操作を独自のリポジトリファイルに定義できます。
postsRepositoryはこんな感じです。
image.png

そしてファクトリを作成します。
image.png

見ての通りリクエストしたリソースのシンプルなオブジェクトが返されます。
この段階でget()メソッドにさらにロジックを追加して複数の環境を扱ったり、モックリポジトリをインポートしたりできることに気付いたでしょう。

たとえば、現在私がいる会社(Snap.hr)では、いくつかの異なる環境が必要です。環境に応じて、モックまたは対応するエンドポイントに変更します。これはあなたが望むものと同じような複雑さでしょう。
でもこの例ではシンプルにしましょう?

.vueファイルのSFCでどのように使用するかお見せしましょう。
image.png
ロジックは完全に分離されているので、異なるパラダイムを持つ別のエンドポイントをGraphQLとして使用することもできます。

?結論

常に銀の弾丸は存在しないものです。このパターンは私のほとんど全部のプロジェクトでうまくいっていますが、あなたのプロジェクトにとって最適という意味ではありません。

しかし、以下のアドバンテージがあると思います。

  • 簡単な方法でテスト可能なコードを実行できます。
  • コンポーネントのコードがきれいになります。
  • 簡単に拡張できますよね?
  • DRYを保つことができます。

実用的なサンプルCodesandbox で作成し、このアプローチを実際に実装してみました。
この種の手法を既に使用しているのであれば、ぜひお聞かせください。
?

提案や質問がありますか?お気軽にコメントやTwitterでコンタクトください!

この記事は2018年に最も人気のあったVue.js記事の1つに選ばれました。


ここまで翻訳

謝辞(Appreciation)

Dear Jorge
Thank you for your amazing article!

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

Nuxt.js(vue.js)で画像を縮小してFirebaseStorageにアップロードする

やりたいこと

  • ユーザーが画像をアップロードできるページを作りたい
  • 確認用サムネイルも表示したい
    • サムネイルは「どんな画像か」がわかるだけの小さなもの
    • アップロードするファイルとは別物
  • フロントで圧縮してからアップすることで通信量を節約したい
    • Firebase Storageの使用量も節約したい
  • サーバーサイドは書きたくない

手順

  1. input[type="file"]でファイルを指定する
  2. new FileReader()でファイル情報を取得する
  3. new Image()でimg要素を作る
  4. img.srcに「2.」のファイル情報を放り込んで画像を作成する
  5. document.createElement('canvas')canvas要素を作る
  6. 「5.」のcanvasに「4.」の画像をリサイズしつつ貼り付ける
  7. あらかじめ用意しておいたサムネイル用のcanvas要素に「4.」の画像をリサイズしつつ貼り付ける
  8. toDataURL('image/jpeg')data_url形式の情報を取得する
  9. FirebaseStorageにアップする

コード

photoResize.vue
<template>
  <div>
    <input type="file" v-on:change="resize" accept=".jpg, .png" ref="input">
    <div>
      <canvas ref="thumbnail" :width="0" :height="0">
      <button v-on:click="reset">×</button>
    </div>
    <div>
      <button v-on:click="upload">upload</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      newImage: '',
    }
  },
  methods: {
    resize(e) {
      const file = e.target.files[0]
      const image = new Image()
      const reader = new FileReader()
      const vm = this

      reader.readAsDataURL(file)
      reader.onload = (e) => {
        image.src = e.target.result
        image.onload = () => {
          vm.newImage = this.width < 1280 ? this.src : vm.makeImage(image)
          vm.makeTumbnail(image)
        }
      }
    },
    makeImage(image) {
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      const ratio = image.height / image.width
      const width = 1280
      const height = width * ratio
      canvas.width = width
      canvas.height = height
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
      return canvas.toDataURL('image/jpeg')
    },
    makeTumbnail(image) {
      const canvas = this.$refs.thumbnail
      const ctx = canvas.getContext('2d')
      const ratio = image.width / image.height
      const height = 120
      const width = height * ratio
      canvas.height = height
      canvas.width = width
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
    },
    reset() {
      const canvas = this.$refs.thumbnail
      this.newImage = ''
      canvas.height = 0
      canvas.width = 0
      this.$refs.input.value = ''
    },
    upload() {
      const photo = this.newImage
      const storage = firebase.storage()
      const ref = storage.ref().child('main.jpg')
      const vm = this
      ref.putString(photo, 'data_url').then(snapshot => {
        console.log('photo uploaded')
        vm.reset()
      })
    },
  }
}
</script>


詳細

HTML部分

<template>
  <div>
    <input type="file" v-on:change="resize" accept=".jpg, .png" ref="input">
    <div>
      <!-- サムネイル用canvas -->
      <canvas ref="thumbnail" :width="0" :height="0">

      <!-- 選択した画像をリセットするためのボタン -->
      <button v-on:click="reset">×</button>
    </div>
    <div>
      <!-- アップロードボタン -->
      <button v-on:click="upload">upload</button>
  </div>
</template>

inputではv-modelを使いたいところですがtype="file"では使えません。
v-on="change"でメソッドを呼び出します。
また、サムネイルを表示するためのcanvas要素を予め書いています。
リセットボタンはあると便利かなという程度です。

画像を作る

  data() {
    return {
      newImage: '',
    }
  },
  methods: {
    resize(e) {
      const file = e.target.files[0]
      const image = new Image()
      const reader = new FileReader()
      const vm = this
      const maxWidth = 1280

      reader.readAsDataURL(file)

      reader.onload = (e) => {
        image.src = e.target.result

        image.onload = () => {
          vm.newImage = this.width < maxWidth ? this.src : vm.makeImage(image)
          vm.makeTumbnail(image)
        }
      }
    },
...
  1. new Reader()でファイル情報を取得
  2. new Image()img要素を作成
  3. 「2.」のimg要素に「1.」のファイル情報を与える
  4. imageの準備が整い次第makeImage()makeThumbnail()を呼び出す

この例では幅1280px以上の画像を1280pxに縮小するものです。
maxWidthの値は用途に応じて調整してください。
画像のwidthmaxWidth以上の場合は、makeImage()で縮小画像を生成、maxWidth以下の場合は入力した画像をそのままnewImageに格納します。

縮小画像を作る

    makeImage(image) {
      // canvas要素を作成
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')

      // 縦横比を算出
      const ratio = image.height / image.width

      // 生成する画像の横幅
      const width = 1280

      const height = width * ratio
      canvas.width = width
      canvas.height = height

      // canvas描画作成
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)

      // data_url形式に変換したものを返す
      return canvas.toDataURL('image/jpeg')
    },

前述の通り、この例では1280pxを基準としています。
用途に応じてwidthの値を調整してください。

ここまででアップロードする画像の作成は完了です。

サムネイルを作成する

    makeTumbnail(image) {
      // 予めHTMLに記述したcanvasを指定
      const canvas = this.$refs.thumbnail
      const ctx = canvas.getContext('2d')

      // 縦横比を算出
      const ratio = image.width / image.height

      // サムネイルのサイズを指定
      const height = 120
      const width = height * ratio

      // canvasの大きさを指定
      canvas.height = height
      canvas.width = width

      // サムネイルに画像を描画
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
    },

この例ではheight="120px"の小さいサムネイルを作成しています。
用途に応じて調整してください。

入力をリセット

    reset() {
      const canvas = this.$refs.thumbnail
      this.newImage = ''

      // サムネイル用canvasのサイズを0に
      canvas.height = 0
      canvas.width = 0

      // inputの入力をリセット
      this.$refs.input.value = ''
    },

画像リセット用メソッドです。
用途に応じて。

画像をアップロードする

    upload() {
      const photo = this.newImage
      const storage = firebase.storage()

      // アップロード先のフォルダ、ファイル名を指定
      const ref = storage.ref().child('main.jpg')

      const vm = this

      // ファイルをアップロード
      ref.putString(photo, 'data_url').then(snapshot => {
        console.log('photo uploaded')

        // 入力をリセット
        vm.reset()
      })
    },

firebaseに画像をアップします。
storage.ref().child('main.jpg')がアップロード先です。
storage.ref().child('hozon/shitai/basho/main.jpg')とすると保存フォルダを指定できます。

まとめ

コンポーネントとして色んなシチュエーションで使いまわしたいということもあると思います。
現場ではprop$emitを使って色んな場面で使えるリサイズ用コンポーネントとして使っています。
その方法もそのうち…

参考

JavaScript で画像をリサイズする方法
ブラウザでローカル画像をリサイズしてアップロード

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

ウィンドウサイズ変更時に一度だけjQueryを実行

問題提起

レスポンシブデザインをつくりましたが、ウィンドウサイズを変えると、jQueryが意図せず発火したり、されなかったり……という問題が発生しました。

以下が正解の挙動です。
Image from Gyazo

以下が問題の挙動です。
Image from Gyazo

Image from Gyazo

正解では、ウィンドウサイズが大きい時にはjQuery(アコーディオン)を機能させず、ウィンドウサイズが小さい時にjQuery(アコーディオン)を発火させています。

しかし問題の例では、ウィンドウサイズを変えても、初めのウィンドウサイズが適応されてしまっています。

コードはこちらです

if (window.matchMedia( '(max-width: 766px)' ).matches){
    $(".title").on("click", function(){
      $(this).next(".element").slideToggle(200);
    });
    $(document).on("click", ".element__parent", function(){
      $(this).next(".element__child").slideToggle(200);
    });
  };

ifの条件が最初のウィンドウサイズにしか適応されないのが問題でした。

つまり、最初のウィンドウサイズが766px以上ならjQueryが機能せず、766px以下ならたとえウィンドウサイズを大きくしてもjQueryが適応されてしまします。

解決策

正解コードがこちらです

$(function(){
  var timer = false;
  $(window).resize(function() {
      if (timer !== false) {
          clearTimeout(timer);
      }
      timer = setTimeout(function() {
      location.reload();
      }, 200);
  });

  if (window.matchMedia( '(max-width: 766px)' ).matches){
    $(".title").on("click", function(){
      $(this).next(".element").slideToggle(200);
    });
    $(document).on("click", ".element__parent", function(){
      $(this).next(".element__child").slideToggle(200);
    });
  };
});

ウィンドウサイズを変化させるごとに画面をリロードするコードを足しました。

コードの説明

var timer = false;
  $(window).resize(function() {
      if (timer !== false) {
          clearTimeout(timer);
      }
      timer = setTimeout(function() {
      location.reload();
      }, 200);
  });

細かいところの説明

・resize()
 ウィンドウサイズが変わるごとにイベントを通知
・setTimeout()
  一定時間経過後に処理を一回だけ実行する
・clearTimeout
  setTimeout()でセットしたタイマーを解除する
・location.reload()
  ページをリロード(再読み込み)する

全体の流れ

まず変数timerをfalseに設定

ウィンドウがresizeされ続けると以下が繰り返される。
・setTimeout()でセットしたタイマーを解除する
・0.2秒後にリロード
・setTimeout()でセットしたタイマーを解除する
・0.2秒後にリロード
・setTimeout()でセットしたタイマーを解除する
・0.2秒後にリロード
・・・

resizeが止まれば最後の「0.2秒後にリロード」が残るので、結果、resize終了時にリロードが一回される。

リロードがされた時のウィンドウサイズによって 

if (window.matchMedia( '(max-width: 766px)' ).matches){
    $(".title").on("click", function(){
      $(this).next(".element").slideToggle(200);
    });
    $(document).on("click", ".element__parent", function(){
      $(this).next(".element__child").slideToggle(200);
    });
  };

の実行が決まるので、結果、ウィンドウサイズを変えたところでjQueryを適応するか否かを判断される。

まとめ

ややこしい話になりましたが、要は

var timer = false;
  $(window).resize(function() {
      if (timer !== false) {
          clearTimeout(timer);
      }
      timer = setTimeout(function() {
      location.reload();
      }, 200);
  });

のコードを入れれば、resize終了時に画面が一度だけリロードされるようになります。

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

【2019年5月】{N} + ng + Firebase : 準備編

NativeScript + Angular で Firebase をバックエンドにしてみたい。

先月の記事 【2019年4月版】NativeScript+Angularで感じた新時代{N} + ng = ❤ に入門いたしましたが、バックエンドに Firebase 使えないのか?!ということで調べてみました。

あっさりと以下のように Firebase プラグインを発見いたしました。これだけでいけるのか?!

また以下の記事で NativeScript での Firebase 利用についてさっくりとご紹介いただいておりました。

これはいろいろと夢が膨らみますね!

さっそく手元で実際の構築手順を再現してみました。

いろいろ手間取りましたので本日は "Firebaseに接続する" ところまでの「準備編」といたします。

対象環境

OS : macOS High Sierra
Android SDK : platform 27 revision 3
実機 : Android 7.1.1
NativeScript-Angular : 7.2.0

前回とは違い、今回は macOS での構築です。

環境の構築手順

NativeScript のインストール

{N} のインストールには以下の公式サイトの構築手順に従ってセットアップを進めて行けばOKです。

yarn の対応はイマイチのようなので諦めて npm を使用します。

$ npm install -g nativescript

ここでは紹介しませんが以下のバージョンの java や Android SDK のインストールをお願いします。

  • JDK 8
  • Android SDK
  • Android Support Repository
  • Google Repository
  • Android SDK Build-tools 28.0.3 or a later stable official release
  • Android Studio

注意点は JDK 8Android SDK Build-tools 28.0.3 ですね。バージョンは確認しておきましょう。

またインストール中に iOS 用に xcode のインストールをしつこく勧めてきますが、Android だけの場合は Skip Step and Configure Manually で飛ばしてOKです。

念のため、以下のコマンドで Android SDK などが正しくインストールされているか確認しましょう。

$ tns doctor

プロジェクトの作成

以下のコマンドでプロジェクトを作成します。

$ tns create HelloWorld --template tns-template-blank-ng

生成される package.json の idorg.nativescript.HelloWorld とあると思います。

これがこのアプリケーションのIDとなります。

firebase のプロジェクト準備

firebase でのプロジェクト作成と、"Android" 用の gooogle-services.json の取得、app.gradle の修正をFirebaseの案内手順通りに行います。
上記のアプリケーションのIDを貼り付けてください。

  1. gooogle-services.jsonHelloWorld/App_Resources/Android/google-services.json に保存します。
  2. app.gradle を以下のように修正します。
app.gradle
dependencies {
  implementation 'com.google.firebase:firebase-core:16.0.1'
}

android {
  defaultConfig {
    minSdkVersion 17
    generatedDensities = []
  }
  aaptOptions {
    additionalParameters "--no-version-vectors"
  }
  project.ext {
    googlePlayServicesVersion = "+"
  }
}

apply plugin: 'com.google.gms.google-services'

具体的には dependenciesの追加、googlePlayServicesVersion = "+"の追加、"apply plugin:" の追加を行なっています。

firebase プラグインのインストール

以下の Readme のセットアップ手順でだいたいOKです。

肝は Firebase の google-services.json の準備が整ってから、firebase プラグインをインストールする、という点でしょう。
以下のコマンドで firebase プラグインをインストールします。

$ tns plugin add nativescript-plugin-firebase

ここで firebase の様々な機能について 有効にするか? と聞いてきます。
お好みに応じて y としておきましょう。

以上でプロジェクトの準備は完了です。

実装手順

今回は firebase への接続を確認するだけなので、app.component.ts を以下のように修正するだけです。

app.component.ts
import { Component, OnInit } from "@angular/core";

const firebase = require("nativescript-plugin-firebase");

@Component({
    moduleId: module.id,
    selector: "ns-app",
    templateUrl: "app.component.html"
})
export class AppComponent {
  // using async-await just for show
  ngOnInit() {
    try {
      firebase.init({
        persist: false
      });
      console.log(">>>>> Firebase initialized");
    } catch (err) {
      console.log(">>>>> Firebase init error: " + err);
    }
  }
}

これでアプリケーションの起動時に、コンソールのログで >>>>> Firebase initialized と出力されればまず、第一関門突破です。

いつものように preview ・・・?

AppComponent の生成がうまくいくか確かめたいので、例によってプレビューアプリで確認してみます。

$ tns preview

ところが起動時にエラーが発生して {N} Preview アプリにログが表示されてしまいます。。。

ログメッセージでググって見た結果、firebase plugin は {N} preview のアプリには含まれていないので、 tns preview コマンドでは動かないとのこと。。。

Like I said: it's not part of the Preview app, so you can't use that.

・・・はい。先に言ってね〜

Android 実機でのデバッグ

というわけで実機で動かす必要ありです。以下のコマンドのヘルプを参照しまして・・・

https://docs.nativescript.org/tooling/docs-cli/project/testing/debug-android

USB で実機を接続後、

$ tns device android

Connected devices & emulators
Searching for devices...
┌───┬─────────────┬──────────┬───────────────────┬────────┬───────────┐
│ # │ Device Name │ Platform │ Device Identifier │ Type   │ Status    │
│ 1 │ XXXXXXXX    │ Android  │ J5AXHM00S6047MX   │ Device │ Connected │
└───┴─────────────┴──────────┴───────────────────┴────────┴───────────┘

で認識されているか確認。Device Identifier をコピーして以下のコマンドで貼り付けます。

$ tns debug android --device J5AXHM00S6047MX

しばらくビルドが進んでおりましたが・・・以下のようにエラーが発生しました。

* What went wrong:
Execution failed for task ':app:transformDexArchiveWithExternalLibsDexMergerForDebug'.
> com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: 
  The number of method references in a .dex file cannot exceed 64K.
  Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html

・・・はい、ということで app.gradle を修正します。

app.gradle
android {
  defaultConfig {
    minSdkVersion 17
    generatedDensities = []
    multiDexEnabled true
  }
  aaptOptions {
    additionalParameters "--no-version-vectors"
  }
  project.ext {
    googlePlayServicesVersion = "+"
  }
}

multiDexEnabled true を追記いたしました。

再度、気を取り直して・・・

$ tns debug android --device J5AXHM00S6047MX
...
Project successfully built.
Installing on device J5AXHM00S6047MK...
Successfully installed on device with identifier 'J5AXHM00S6047MX'.
Restarting application on device J5AXHM00S6047MX...
JS: >>>>> Firebase initialized
JS: Angular is running in the development mode. Call enableProdMode() to enable the production mode.
device: J5AXHM00S6047MX debug port: 40000

To start debugging, open the following URL in Chrome:
chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:40000

Successfully synced application org.nativescript.HelloWorld on device J5AXHM00S6047MX.

動いたー!!!
いろいろありましたがついに >>>>> Firebase initialized のコメントを拝むことができました!!
これで firebase オブジェクトが使用可能になった模様です!

初回のビルドは例によって非常に時間かかります。その間、気長にお待ちください。
(私は実機の接続が必須と知ってから Android SDK 28 をインストールし始めたのでさらに時間がかかってしまいました。。。)

引き続き {N} + Firebase の開発で firebase の各種機能についていろいろ試してみたいと思います。

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

プログラミング学習記録85〜スライドショー少しだけ〜

今日やったこと

  • ドットインストール「JavaScriptでスライドショーを作ろう」を半分だけ

今日もまたやる気がなくなってしまったのですが、なんとか少しだけ進めました。

vertical-alignで位置を調整する方法は初めてやりました。
今の所そこまで使うメリットは感じないのですが、細かい位置の調整に使えそうです。

Progateではよく使っていたfloatは、ドットインストールでは全然使われないですね。

過去にはよく使われたのかもしれないですが、今はあまりメジャーな手法では内容です。

レスポンシブ対応のことも考えるとflexboxの方が明らかに使いやすいですね。

ということで、また明日からも少しづつ進めていこうと思います。

おわり

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

Ajax通信によってセレクトボックスを出現させる

概要

Ajax通信を利用し、あるアクションによってセレクトボックスを出現させます。

↓完成形
Image from Gyazo

困ったポイントは、選択するものによってselectタグのoptionの数が違うということです。

今回でいうと、
レディース トップス/パンツ の2つ
メンズ   トップス/パンツ/バッグ/靴 の4つ

条件によって、ビューに挿入する要素の長さを変えなければいけません。

完成コード

 まずは完成コードがこちらです。

category
$("#range").change(function(){
    $("#range-second").remove();
    $("#range-third").remove();
    var category = $('option:selected').val();

    function buildHTML(children){
      var option = ``
      children.forEach(function(child){
        option += `<option value="${child.id}">${child.name}</option>`
      });

      var html = `<select name="range-second" id="range-second">
                    <option value label=" "></option>
                    ${option}
                   </select>`

      return html;
    };

    $.ajax({
      url: '/users/1/products/new',
      type: "GET",
      data:{category: category},
      dataType: 'json'
      })
      .done(function(data){
        var html = buildHTML(data);
        $(".products_new_container__content__select__box__category").append(html);
      })
      .fail(function(){
        alert('error');
      })
  });

このなかで、今回のポイントとなるコードはこの部分です。

function buildHTML(children){
      var option = ``
      children.forEach(function(child){
        option += `<option value="${child.id}">${child.name}</option>`
      });

      var html = `<select name="range-second" id="range-second">
                    <option value label=" "></option>
                    ${option}
                   </select>`

      return html;
    };

一行目のbuildHTML関数の引数「children」には、selectタグの選択肢が配列として格納されています。

二行目で、まず空の変数optionを宣言します。

その次でforEachメソッドを使用し、childrenに含まれた要素の数だけ変数optionの中にoptionタグを追加していきます。

あとは、必要なだけのoptionタグの入った変数optionを、ビューに挿入する変数htmlのなかに入れるだけです。

まとめ

まず空の箱を用意して、配列の要素をeachでひたすら詰めていくというのは、Rubyでもよくやりました。

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

QAエンジニアの異常な愛情〜または私は如何にして心配するのを止めてCodeceptJSを愛するようになったか

はじめに

今から約1ヶ月前の2019年4月1日に、TestCafeで記述したE2Eテストを全て削除し、全面的にCodeceptJSに切り替えました。

image.png

File Changedが大変なことになっていますが、基本的にはディレクトリ構造を変え、重複していたコードを削除しただけですので大した内容ではありません。
(お作法とかはギュッと目をつぶって頂けると助かります)


自分がTestCafeを使い始めたのは、今からおよそ1年前、2018年6月頃でした。
SeleniumやPuppeteerなどのメジャーなライブラリにはない様々な特徴が気に入り、およそ半年ほどの利用を経て、2018年12月のOPENLOGI Advent Calendarでは、『個人的ベストE2Eフレームワーク"TestCafe"の紹介』 という記事を書きました。

それからさらに1ヶ月ほどが経ち、正月休みで暇を持て余していた自分は、何を思ったか Awesome Selenium で紹介されているライブラリを片っ端から試していくという荒行を開始しました。

その時に出会ったのがCodeceptJSです。
受け入れテストをそのまま動かすことを強く意識した特徴的な記法を受け入れるのに当初は時間を要しましたが、使っていくうちに、テストを読みやすく効率的に書くための様々な機能に惚れ込み、残っていた休暇と、休暇明けからしばらくの土日は全てCodeceptJSの検証作業に費やしました。
余談ですが、検証中に得られた知見をQiitaにアウトプットしまくったところ、一時的にOrganizationランキングの上位に弊社がランクインしていました。
image.png

検証の結果、いくつか実運用上問題のあるバグが存在していたため、かなり小粒でしたがいくつかPullRequestを出し、mergeされました。
(これまた余談ですが、これが自分にとって初めてのOSSコントリビュートでした)
テストケースの移行に約1ヶ月、ライブラリのバグの修正や、CIのメンテナンス、テストの安定性の向上などに約2ヶ月ほどを要し、計3ヶ月ほどでTestCafe→CodeceptJSへの乗り換えが完了しました。


移行を決めた段階で、TestCafeで書かれたテストケースは20件ぐらいで、移行にそれほど長い時間がかからないと想定され、実際、実業務の片手間でコツコツすすめて3ヶ月で完了するといったレベルでした。
とはいえ、既に実稼働しているテストコードを破棄し、全く設計思想の異なるライブラリに移行することはかなりの勇気と情熱が必要です。
また、テストコードそのものの移行だけでなく、ライブラリの設定や、CIなどの実行環境構築も必要でしたので、当初想定していたより時間がかかりました。

なぜ、それほどの手間をかけてまで、ライブラリの変更という決断に至ったのか?
というわけで今回は、TestCafeが大好きだった自分が、なぜCodeceptJSに乗り換えたのかについて書きたいと思います。

※かならずお読みください※

この記事は2019年1月ごろまでのTestCafeの利用経験を元に書いていますが、当時まだTestCafeはパブリックベータ版でしたが、その後、2019年2月に正式版がリリースされています。
本記事で紹介している様々な不具合や仕様上の問題点については、現時点では改善されている可能性があります。
そのため、TestCafeの利用を検討している方は、 絶対にこの記事を鵜呑みにせず 、ご自身の手で検証した上で判断してください。

そもそもなぜTestCafeを使おうと思ったのか

先述した記事『個人的ベストE2Eフレームワーク"TestCafe"の紹介』でも書きましたが、

  • 環境構築が容易であること
  • モバイルも含めたクロスブラウザテストが手軽に出来ること
  • 要素が表示されるまで待ってくれること

この3点が大きなポイントでした。
フレームワークの選定にあたって、CypressWebdriverIOなどの他のライブラリと比較したのですが、Cypressはクロスブラウザ、WebDriverIOは環境構築や表示待ちの面で要件を満たせませんでした。
(WebDriverIO、環境構築かなり楽な方だと思うのですが、なぜか当時はSelenium-Standaloneが上手く動作せず……)

なぜTestCafeをやめようと思ったのか

Seleniumでも意外と色々できることを知った

実を言うと、E2Eテストライブラリを比較検討した際、Seleniumはかなり早い段階で候補から外してしまっていました。
Selenium RC時代の苦い思い出もあり、なんとなくレガシーで、SPAには不向きで、環境構築が辛いといったネガティブなイメージが先行していました。

ですが、あるとき SelenideGeb の存在を知り、適切なラッパーライブラリを使うことでSeleniumの欠点をカバーし、SPAのテストもストレスなく実行できる出来ると知りました。
また、環境構築についても、公式で配布しているDockerImageや、Selenium-StandaloneのようなNPMパッケージを利用することでコマンド一発で環境構築が出来ると知りました。

このことで 何となく とか イメージ とか 過去の苦い思い出 とかで技術を評価するのは良くない」ということが身にしみたので、Seleniumも含めていろいろなライブラリを再評価しようとしたのが、TestCafeをやめようと思ったそもそものきっかけでした。

ブラウザ操作部分の実装が枯れておらず、地雷を踏むことが多かった

また、E2Eテスト実装においてコアとなる「ブラウザ操作」部分の安定性において、TestCafeはいまいちだと思うケースがありました。

どういうことかというと、TestCafeのブラウザ自動操作は、WebDriverではなく、URL rewriting proxyという独自実装で実現されています。
https://devexpress.github.io/testcafe/faq/#i-have-heard-that-testcafe-does-not-use-selenium-how-does-it-operate

この方式を取ることで、TestCafeは

  • 環境構築に特別な準備がいらない(WebDriver不要)
  • モバイルも含めたクロスブラウザテストが可能
  • 要素の表示待ちを自動で実行してくれる
  • 実行が早い(らしい、あんまり実感したことはない……)

などの機能を実現しており、いわばTestCafeの最大の特徴であり根幹となっている部分なのですが、一方でこの部分が枯れていないがゆえに、変なところで不具合に遭遇することも非常に多かったです。
例を挙げるとこんな感じです。

  • Edgeの最新バージョンでTextareaへの文字入力ができない など、ブラウザのバージョンアップに伴いバグが出ることがあった
  • 縦横のスクロールが可能なサイトで、スクロールが上手くいかず要素のクリックに失敗することがあった
  • 手動でブラウザを操作しているときは普通に動作するのに、なぜかTestCafe経由では謎のエラーで画面描画すらされない

その点、Seleniumで用いられるWebDriverという仕組みは、各ブラウザベンダーが対応するWebDriverを開発・メンテナンスしているため、ブラウザ操作部分においては信頼性が高いという利点があります。

Seleniumのエコシステムを活用できない

TestCafeは非Selenium系のライブラリですので(Selenium or not Seleniumみたいな区分けもアレだと思いますが)、たとえばコンテナによる分散実行をサポートしてくれるZaleniumSelenoidなどを利用することができません。

現在、TestCafeはVideo Recordingをサポートしていますが、自分が使っていた当時はまだ未実装で、失敗したテストケースのログを後から追うのが非常に苦痛でした。
上述のZaleniumを使えれば、自動でセッションごとの動画が記録されるのですが、こうした便利なツールが利用できないことも残念なポイントでした。

CodeceptJSとの出会い

手前味噌で恐縮ですが、以前『エンジニア視点で見たCodeceptJS』という記事をQiitaに投稿したことがありました。
この記事はこんな見出しで始まります。

CodeceptJS?ああ、あのキモい書き方のやつでしょ?

何を隠そう、「はじめに」で書いた Awesome Selenium を片っ端から試すという荒行の過程で、CodeceptJSは書き方がキモいという理由で試すのを後回しにしてしまっていました。
ですが、実際試してみると、TestCafeを使いはじめた動機となった

  • 環境構築が容易であること
  • モバイルも含めたクロスブラウザテストが手軽に出来ること
  • 要素が表示されるまで待ってくれること

が全て満たせるということと、ブラウザ操作部分をSeleniumやPuppeteerなどのライブラリに任せることで、不具合が少なく動作が安定するという利点がありました。

また、当初はなじめなかった独特の記法も、慣れてみると非常に読みやすく、メンテナンス性が高いと思うようになりました。

  • 受け入れテストのテストスクリプトを、そのままテストコードに落とし込むことができる
  • 同期的に書くことができ、 await を書く必要がない
  • Semantic LocatorによりCSSセレクタやXPathから解放される

このように、受け入れテストを効率よく自動化することに徹底的にこだわった姿勢を自分は非常に気に入りました。

導入までのプロセス

TestCafeで書かれたコードをCodeceptJSベースに移植した

もともと、TestCafe導入初期に書かれたコードはかなり試行錯誤しながら書いていたので、単純な書き換えでは技術的負債もそのまま移植することになるのが目に見えていました。
その為、リリース前に手動で実施しているリグレッションテストの項目を元に、ほとんどのテストを再実装しました。
移行期間中は、CIでの実行はCodeceptJSのみ行い、リリース前日のリグレッションテスト実行の際にCodeceptJSとTestCafe双方でテストを実行する、という形式を採用しました。

CIツールの準備

TestCafe時代からGitLab CIで定期実行をしていたので、基本的な部分は変わらなかったのですが、CI内でdocker-composeを利用するために、Docker-in-Docker(dind)などの利用が必要でした。
下記の記事を参考にしました。
https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#making-docker-in-docker-builds-faster-with-docker-layer-caching

導入の結果

テスト実行が安定した

image.png

E2Eテストにつきものの「よく分かんないけどしょっちゅうテストが落ちる」という状況は、CodeceptJSに移行してからかなり改善されました。
retryFailedStepという公式プラグインがあり、クリックやアサーションなどのステップが失敗した際、自動でそのステップのみ再実行してくれるのですが、例えば「クリックしようとした要素に通知トーストが被さっていて実行に失敗した」などでテストに失敗するケースがかなり削減されました。

コード量が削減された

TestCafeでは、「ある文字列を持つ要素」を選択するためには、次のような記述をする必要がありました。

const button = Selector('button').withText('送信')
await t.click(button)

const field = Selector('input').withAttribute({placeholder: '検索'})
await t.typeText(field, 'TestCafe')

一方で、CodeceptJSでは、シンプルに文字列を指定するだけで実現できます。

I.click('送信')
I.fillField('検索', 'CodeceptJS')

CodeceptJSにはこの他にも、テストコードの実装を楽にしてくれる様々な機能がある為、コード量が減り、読みやすく書きやすいテストコードが実現できています。

これからの課題

CodeceptJSのユーザー・Contributor数が少ない

OSSを利用する上で、コミュニティが活発かどうかは非常に重要なファクターで、CodeceptJSはこの点でまだ課題があると考えています。
例えば、GithubのStar数で比較すると、2019/5/12時点で、TestCafeは6,716、CodeceptJSは2,208と、約1/3程度です。

image.png

そんな事情もあり「とりあえず知名度だけ上げてみよう」と書いたのが、『我が名は神龍……どんなテストもひとつだけ自動化してやろう』という記事です。
ありがたいことに、1400件を超えるいいね!を頂き、現在も少しずつ増え続けています。

公開当時、QiitaのCodeceptJSタグのフォロワーは0人だったのですが、公開後は5人になりました。
0→5なので、増加率的には無限大ですね。大躍進です。

冗談抜きで、おれ自身は大体どこに行っても神龍の人で通じるようになってきたのですが、本来の目的であったCodeceptJSの知名度はそんなに上がってない気がするので、頑張っていきたいです。

とはいえ、まったく状況が改善していないわけではなく、少しずつですが「使ってみた」系の記事が増えるようになってきました。
なかでも、先日公開された @Kesin11 さんの記事『E2Eテストの面倒くさいことはCodeceptJSにお願いしよう』は出来ることが非常によくまとまっており、大変おすすめです。
また、クロスブラウザテストの環境構築まで含めた解説記事としては、別の方の『Vagrant + Selenium + node.js(CodeceptJS)でIE, Chrome, FirefoxのマルチブラウザE2Eテスト』というのもあり、こちらも大変おすすめです。

なお、CodeceptJSはGithubの他、discourseフォーラムやSlackなどで質問を受け付けています。
興味のある方は是非ご参加ください。

テスト実行時間が長い

かなり最適化を進めたとはいえ、約40ケースの実行に30〜40分程度の時間がかかっています。
テスト実行時間は開発効率、開発体験に直結するので、先程も登場したZaleniumなどを用いて分散実行させ、実行効率を上げていきたいと思っています。

ところでTestCafeのことは嫌いになっちゃったの?

いえ、大好きです
導入コストは低く、出来ることは多い、素晴らしいライブラリだと思います。

今後有償になるそうですが、TestCafe StudioというRecord&Playbackツールも用意されています。
Selenium, Cypress, Puppeteerなどと同様、今後もE2Eテストツールの有力な選択肢の一つとして開発が続くことを期待しています。

おわりに

TestCafeとCodeceptJSを通算で約8ヶ月ほど実運用しましたが、実はこれによって不具合を発見したのはたった1件です
E2Eテストは実装難易度が高く、実行時間は長く、それでいてカバーできる領域は少ない、自動テストの中では非常にコストパフォーマンスの悪い分野です。

とはいえ、これが無ければ防げないバグがあるのは事実ですし、これがあることで開発者が安心して開発できるのも事実です。
であれば、出来る限り実装コストが低く、実行時間が短く保てる方法を常に模索し、コストとパフォーマンスのバランスを良くしていくことは、QAエンジニアの責務の一つでしょう。
今回のライブラリ乗り換えや、それに伴うライブラリへのContributeはそうした取り組みの一つであると思っています。

今回、自分はたまたまCodeceptJSに出会い、それが最良の選択肢であると信じ採用しましたが、少なくとも自分が見た範囲で「CodeceptJSが最高のE2Eテストフレームワークである」と結論付けている記事は見つかりませんでした。
もし自分が、自分の手でいろいろなライブラリを試すということをしなければ、CodeceptJSという選択肢には絶対にたどり着かなかったでしょう。

ソフトウェアの世界が日進月歩であるのと同様、ソフトウェアテストの世界もまた目まぐるしく進歩し続けています。
例えば、先日のSeleniumConfTokyoにおいて、Selenium4の主要な機能がいくつか発表されましたが、その中には、CodeceptJSが既に実現しているアイディアもあれば、「その手があったか!」と唸ってしまうような素晴らしいアイディアもありました。
Seleniumもまた、使いやすくストレスレスなライブラリに進化していこうとしています。

また、AIをテストに活用しようという動きも活発に出ており、mablTestimなど既にサービスが稼働しているものも存在します。
これらはE2Eテストにありがちな「テストが壊れる」「テストケースの作成に時間がかかる」などの問題に、機械学習のテクノロジーによって立ち向かい、E2Eテストを楽にしようとしてくれています。

世の中には「このライブラリがベスト!」「このフレームワークを使うべき!」と言った論調の記事も多く存在していますが、次の日にはより優れたライブラリが登場したり、昔からあるライブラリがバージョンアップしてより魅力的になることもあるかもしれません。
行く先々、その時々で最適な選択肢を採ることが出来るよう、日頃からいろいろなライブラリやフレームワークに触れておくようにしたいですね。

…………というわけですので、CodeceptJS、食わず嫌いせず一度触ってみてください。最高なので

おまけ

Qiita Jobsでエンジニア募集してます 。来てね。

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

私がHTML の <select> 要素 multipleに求めること(動くサンプルコードあり)

HTML の 要素 multiple

はじめに

まずは下記の画像とマークアップをご覧ください。

スクリーンショット 2019-05-11 18.37.12.png

index.html
<div class="box">
  <p>select</p>
  <select name="select1">
    <option value="en">英語</option>
    <option value="de" disabled>ドイツ語</option>
    <option value="fr" selected>フランス語</option>
    <option value="ja">日本語</option>
    <option value="zh">中国語</option>
  </select>
</div>

<div class="box">
    <p>select multiple</p>
    <select name="select2" multiple>
    <option value="en">英語</option>
    <option value="de" disabled>ドイツ語</option>
    <option value="fr" selected>フランス語</option>
    <option value="ja" selected>日本語</option>
    <option value="zh">中国語</option>
  </select>
</div>
style.css
.box{
  border: 1px solid;
  margin: 1rem;
  padding: 1rem;
}

何の変哲も無いselect要素です。

この記事ではHTMLのselect要素のUIについてもう一度考えてみました。

皆さんもご存知のように上の画像は、選択肢が限られている際に選択するUIとしてWebアプリケーションで頻繁に使う select要素です。

select要素にmultiple属性を与えると上の画像でいうと下のように複数選択が可能なselect要素になります。

モバイル

私は、モバイルでのselect multipleには不満がありません。

どのように動作するかは下記の画像のようになります。

select要素multiple選択時
スクリーンショット 2019-05-11 18.53.57.png

選択時のインターフェースはandroid,iosで違えど、各OSでのデフォルトのUIなのでそれぞれのユーザーにとって馴染みがあり操作しやすく、多くの方が複数選択も問題なく選択できると思います。

PC

PCでの不満点が2つあります。
それは

  • 操作が少し難しい
  • 表示が変わる

と思っています。

操作が少し難しい

複数選択するためには、
複数の選択はshiftを押しながらクリックで、最後に選択したところからクリックしたところまでの全てを選択します。また、単体で複数を選択する際には
macならcmd+クリック
windowsならctrl+クリック
になります。もうちょっと簡単だったらいいのに。

表示が変わる

multiple が指定されている場合、多くのブラウザーは単一行のドロップダウンの代わりに、スクロールするリストボックスを表示します。(単一のselect要素にsize="4" の属性が付与されているのと同じ表示になります。)
表示に関してはモバイルとPCで問題が発生しそうなポイントですね。

ライブラリ

こういう時に先人の力を借りるべくライブラリを探すわけです。
自分が使ったことがあり、PCとandroid,iosで確認した複数選択ができるライブラリは下記になります。

どれも優秀なライブラリーですが、各ユーザーのデバイスでの慣れや一貫性を考えると、モバイルでのネイティブのUIには劣ってしまうなーと感じます。(bulmaに関してはスタイルをつけているだけなので、モバイルはいいけど、PCでの操作と表示の問題が発生します。)

なぜブラウザのデフォルトUIを求めるのか

見た目にこだわって選択したライブラリが
・思っていたものよりも高機能でコードサイズが大きかった経験ないですか?
・使い心地がPCもしくはモバイルでいまいちだった経験ないですか?
O(N^2)で計算時間が増加しパフォーマンスが劣化した経験はないですか?
・機能性を追求して結局求めていたものはデフォルトのUIだったみたいな経験ないですか?

作るものによるとは思いますが、基本的にかっこいいからとか可愛いからという理由で自分はUIはなるべくサードパーティを使いたくないですね。
各ユーザーのデバイスでの慣れや一貫性をアプリのレイヤーではなくOSのレイヤーで考えるとブラウザのデフォルトのUIで完結している場合も多い思うんです。

また、ブラウザのデフォルトのUIは安定しているし、パフォーマンス的に考えられていたり、キーボードのショートカットがきちんと割り当てられているので、表面上の「映え」よりも結果的にUXに優れています。

デフォルトUIが見た目的に嫌いでも、スタイリングすればいいのです。本当にライブラリは必要ですか?
UIフレームワークで最近人気の「Bulma」ってありますがスタイルを変えているだけなので、デフォルトの挙動に対してスクリプトで変なことしてないので好きです。

どうなってほしいのか(動くサンプルコードあり)

モバイルに関しては不満がないので、PCの場合だけ少し変えてみたいと思います。
先ほどあげたように

  • 操作が少し難しい
  • 表示が変わる

を改善します。

単一の選択肢の際のselect要素をベースに、アクティブになった時に複数選択のリストボックスを表示してみたいと思います。また、クリックすることで選択のオンオフを切り替えられるようにします。

勢いで書いたのでクロームでしか試していません。一応タッチデバイスではデフォルトのUIが出るようにしています。

動作
ezgif.com-gif-maker.gif

コード
https://stackblitz.com/edit/native-multiple-select?file=index.js

まとめ

・多分ブラウザの下位互換や歴史的にUIが変更されることはないと思うので結局自分でライブラリか何かを作るしかないんだろうねー
・ひさびさにDOMを触って昔を思い出した
・Reactなどでラップして小さいコンポーネント作ろうかなと思った

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

TensorFlow.jsでDeepLearning(Making Predictions from 2D Data - 前半)

こんにちわ。Electric Blue Industries Ltd.という、ITで美を追求するファンキーでマニアックなIT企業のマッツと申します。TensorFlow.jsでDeepLearningのチュートリアル「Making Predictions from 2D Data」の詳細解説の前半です。

これはTensorFlow.JSの公式サイトにある「TensorFlow.js — Making Predictions from 2D Data」をコードの中に記載したコメントで詳細に解説したものです。解説の利便性によりコードの部分の位置関係は変更してありますが、内容に変化はありません。実際に動作するデモはこちらで見られます。

1. コード

1.1. html

ライブラリを読み込んでDeep LearningのためのJavaScriptを実行するHTMLです。

index.html
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TensorFlow.js Tutorial - Making Predictions from 2D Data</title>

    <!-- Import TensorFlow.js (TensorFlow.jsライブラリ本体を読み込みます) -->
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.js"></script>
    <!-- Import tfjs-vis (TensorFlow.js向けの可視化ライブラリを読み込みます) -->
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>
    <!-- Import the main script file (下記に解説するJavaScriptを読み込みます)-->
    <script type="text/javascript" src="script.js"></script>
</head>
<body>
</body>
</html>

1.2. JavaScript

上記のHTMLにインクルードされてDeep Learning処理を行うJavaScriptです。

script.js
/******************************************************************
TensorFlow.js — Making Predictions from 2D Data
url: https://codelabs.developers.google.com/codelabs/tfjs-training-regression/index.html#0
filename: script.js
copyrighted to: tensorflow.org
composed by: Mats (Electric Blue Industries Ltd.)
description: 車のスペック情報から「燃費」「馬力」のデータを学習し、それらの相関を学習させる
******************************************************************/

async function run() {

    //******************************************************************
    // 1. getData: 元データをgetDataを使って読み込み対象アイテムのみフィルター(元データプロット用)
    //******************************************************************

    // 非同期で元データを取得(getDataは下で関数として定義)
    // なお、取得するデータは [{0:a, 1:b, 2:c}, {0:d, 1:e, 2:f}]のように、各クルマのスペック情報を持つオブジェクトが要素になった配列データ
    const data = await getData();

    // 読み込んだ元データであるdataから項目抽出して、座標情報オブジェクトを各要素とする配列として新たに格納
    const values = data.map(d => ({
        x: d.horsepower,
        y: d.mpg,
    }));

    // TensorFlowJSの可視化ライブラリで元データの位置をプロットする
    tfvis.render.scatterplot(
        // 表のタイトルの指定
        {name: 'Horsepower vs Miles Per Gallon'},
        // 上記で生成した座標情報オブジェクトを各要素とする配列を指定
        {values}, 
        // 表のx軸y軸のタイトルおよび表の高さを指定
        {
            xLabel: 'Horsepower',
            yLabel: 'Miles Per Gallon',
            height: 300
        }
    );

    // 元データを非同期で読み込む関数
    async function getData() {

        // 非同期でクルマのスペックデータ(内容はオブジェクトを要素に持つ配列のフォーマットをしている)を取得しcarsDaraに格納
        const carsDataReq = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');
        // 読み込んだデータをJSON形式として読んで各クルマのスペック(オブジェクト)を要素として持つ配列carsDataとして格納
        const carsData = await carsDataReq.json();
        // 格納した配列carsDataデータから元データとして用いる2アイテムのみを取得しcleanedとして格納
        // map関数は配列の各要素に繰り返し指定された処理を行うので、cleanedはmpgとhorsepowerの情報のみを含むオブジェクトを要素とする配列
        // なお、mpgとhorsepowerのどちらかでも空文字の場合はデータから削除しておくfilterをかける
        const cleaned = carsData.map(car => ({
            mpg: car.Miles_per_Gallon,
            horsepower: car.Horsepower,
        }))
        .filter(car => (car.mpg != null && car.horsepower != null));

        return cleaned;

    }

    //******************************************************************
    // 2. createModel: モデルの枠組みの作成
    //******************************************************************

    // モデルを作成(createModelは下で関数として定義)
    const model = createModel();
    // 上記で作成したモデルの要約情報(Layer Name, Output Shape, # Of Params, Trainable)表示
    tfvis.show.modelSummary({name: 'Model Summary'}, model);

    // モデル作成の関数定義
    function createModel() {

        // シーケンシャルモデル(線形回帰モデル)の枠組みの作成
        // これはモデルが全体として線形回帰モデルになるという意味ではなく、各ニューロンの入出力の関係が y=Σ(wx)+b と書ける線形回帰モデルであるということ
        // なので、モデル全体として非線回帰モデル(二次関数や三次関数など)かどうかという話ではないので心配無用。
        const model = tf.sequential();

        // 入力層を追加
        model.add(tf.layers.dense({
            // 入力は 1x1 のテンソル(=スカラー)
            inputShape: [1],
            // ユニット(別名:ノード)は1個だけ
            units: 1,
            // y=Σ(wx)+b となる定数項bであるバイアスを使用する
            useBias: true
        }));

        // ここに中間層を追加した場合にはどうなるのかは別途に言及する

        // 出力層を追加
        model.add(tf.layers.dense({
            // ユニット(別名:ノード)は1個だけ
            units: 1,
            // y=Σ(wx)+b となる定数項bであるバイアスを使用する
            useBias: true
        }));

        return model;

    }

    //******************************************************************
    // 3. convertToTensor: 学習データを上記モデルに流し込めるようテンソルに変換する
    //******************************************************************

    // getDataで取得したclean済み配列データを下記で定義したconvertToTensor関数でテンソルに変換(不要なアイテムは同時にフィルター)
    const tensorData = convertToTensor(data);
    const {inputs, labels} = tensorData;

    // 学習データをテンソルに変換する
    function convertToTensor(data) {

        // tidyを使って計算することで、計算経過で生成される変数をメモリから削除してメモリにゴミを残さない
        return tf.tidy(() => {

            // (ステップ1) データをシャッフルする 

            tf.util.shuffle(data);

            // (ステップ2) データを配列に格納してから展開してテンソルに変換

            // 変数inputに馬力に関するデータ(実際は配列)を格納
            const inputs = data.map(d => d.horsepower)
            // 変数labelsに燃費に関するデータ(実際は配列)を格納
            const labels = data.map(d => d.mpg);

            // 上記で作成したインプット値(馬力)の配列を使って Nx1 の行列を生成
            const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
            // 上記で作成される2次元テンソルは下記のような縦長の形
            // [[馬力の値1],
            //  [馬力の値2],
            //    :
            //  [馬力の値N]]

            // 上記で作成したラベル値(燃費)配列を使って Nx1 の行列を生成
            const labelTensor = tf.tensor2d(labels, [labels.length, 1]);
            // 上記で作成される2次元テンソルは下記のような縦長の形
            // [[燃費の値1],
            //  [燃費の値2],
            //    :
            //  [燃費の値N]]

            // (ステップ3) 入力データの値を0から1の間に正規化

            // 入力とラベルを最大値と最小値を調べて取得
            // 入力である馬力の最大値を取得
            const inputMax = inputTensor.max();
            // 入力である馬力の最小値を取得
            const inputMin = inputTensor.min(); 
            // 出力である燃費の最大値を取得 
            const labelMax = labelTensor.max();
            // 出力である燃費の最小値を取得 
            const labelMin = labelTensor.min();

            // 入力とラベルを正規化
            // inputTensor.sub(inputMin)で各入値から最小入力値をひく(=最小値をゼロに落とす) >> (これをAとする)
            // inputMax.sub(inputMin)で最大入力値から最小入力値をひく >> (これをBとする)
            // 上記の各(A)の値を(B)で割ることで、最大値が1で最小値が0になるよう正規化する
            const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
            // labelTensor.sub(labelMin)で各入値から最小入力値をひく(=最小値をゼロに落とす) >> (これをA'とする)
            // labelMax.sub(labelMin)で最大入力値から最小入力値をひく >> (これをB'とする)
            // 上記の各(A')の値を(B')で割ることで、最大値が1で最小値が0になるよう正規化する
            const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

            return {
                // 正規化された値を要素に持つ入力テンソルと出力(ラベル)テンソルを返す
                inputs: normalizedInputs,
                labels: normalizedLabels,
                // 入力と出力(ラベル)の最大値最小値もあとで逆正規化できるよう返す
                inputMax,
                inputMin,
                labelMax,
                labelMin,
            }
        });

    }

    //******************************************************************
    // trainModel: モデルの学習
    //******************************************************************

    // モデルの学習(trainModelは下で関数として定義)
    // awaitとすることでtrainModel関数からreturnがあるまで待機する
    await trainModel(model, inputs, labels);

    // モデル・入力(インプット)テンソル・出力(ラベル)テンソルを指定してモデルの学習を行う関数
    async function trainModel(model, inputs, labels) {

        // 学習実行のため、学習方法を指定してモデルをコンパイル  
        model.compile({
            // 最適化法をアダム(=適応モーメント推定法)に指定
            optimizer: tf.train.adam(),
            // 損失関数をMSE(=平均二乗誤差)に指定
            loss: tf.losses.meanSquaredError,
            // 学習とテストに用いる指標(この場合は平均二乗誤差)を表す表現を決める
            metrics: ['mse'],
        });

        // バッチサイズ(小分けにグループ分けした学習データに含まれるデータの個数)を28個とする
        const batchSize = 28;
        // 学習の1手順の回数を50回とする
        const epochs = 50;

        // epochsで指定した回数の学習手順回数(エポック)になるまで学習を実行する
        return await model.fit(inputs, labels, {
            batchSize,
            epochs,
            shuffle: true,
            // 学習結果の随時の描画用にTFVISに、コンパイル時に指定した指標の値をコールバックする指定
            callbacks: tfvis.show.fitCallbacks(
                // 描画する表のタイトル
                { name: 'Training Performance' },
                // 描画する指標(ここではlossとmseを改めて指定)
                ['loss', 'mse'], 
                { 
                    // 表の高さ
                    height: 200, 
                    // コールバックのタイミング
                    callbacks: ['onEpochEnd'] 
                }
            )
        });

    }

    // trainModel関数に返り値があった時点で規定したエポックが終了したことになるので「学習が終わった」とコンソールに出力
    console.log('Done Training');

    //******************************************************************
    // testModel: 学習ずみモデルに入力を与えて出力を得て、元データのプロットと重ねて違いを視覚的に見せる
    //******************************************************************

    // モデルのテスト(testModelは下で関数として定義)
    testModel(model, data, tensorData);


    function testModel(model, inputData, normalizationData) {

        const {inputMax, inputMin, labelMin, labelMax} = normalizationData;  

        // Generate predictions for a uniform range of numbers between 0 and 1;
        // We un-normalize the data by doing the inverse of the min-max scaling 
        // that we did earlier.
        const [xs, preds] = tf.tidy(() => {

            // tf.linespaceで0から1までの間に等間隔となる100個の値を生成(0, 0.01, 0.02, 0.03,・・, 0.98, 0.99)し格納
            // なお、tf.linspace()によって生成されるのは配列ではなく「Array.from()で配列化できるオブジェクト」である
            const xs = tf.linspace(0, 1, 100);
            // 上記で生成した100個の数値を要素にもつ行列を生成し、学習したモデルに予測値として出力させる
            const preds = model.predict(xs.reshape([100, 1]));
            // モデルの入出力は共に正規化されているので、これを元に戻す計算を行う
            const unNormXs = xs.mul(inputMax.sub(inputMin)).add(inputMin);
            const unNormPreds = preds.mul(labelMax.sub(labelMin)).add(labelMin);
            // 上記で非正規化されたデータは100行1列の行列になっているので、これらを単なる配列の形にする
            return [unNormXs.dataSync(), unNormPreds.dataSync()];

        });

        // 学習データのポイント(座標)を配列として格納
        // mapは配列を受け取って、指定した処理を行う
        // ここでは入力値inputDataとして「座標情報を持ったオブジェクト」を要素にもつdataを代入しているので、各要素から馬力と燃費のデータを読んで
        // ポイント(座標)をオブジェクト形式で表す要素を持つ配列の各要素として持たせている
        const originalPoints = inputData.map(d => (
            {
                x: d.horsepower,
                y: d.mpg,
            }
        ));

        // 学習させたモデルを使って算出した予測値のポイント(座標)をオブジェクト形式で表す要素を持つ配列として格納
        const predictedPoints = Array.from(xs).map((val, i) => {
            return {
                x: val,
                y: preds[i]
            }
        });

        // 上記で得た予測値および学習データ値のポイントをTFVISに渡してプロット表示
        tfvis.render.scatterplot(
            {
                // 表のタイトル
                name: 'Model Predictions vs Original Data'
            },{
                // originalPointsとpredictedPointsはポイントの座標をJSONで表現した文字列を要素に持つ配列
                values: [originalPoints, predictedPoints],
                series: ['original', 'predicted']
            },{
                // 縦軸と横軸の名称、表の高さ
                xLabel: 'Horsepower',
                yLabel: 'Miles Per Gallon',
                height: 300
            }
        );

    }

}

document.addEventListener('DOMContentLoaded', run);

2. 実行結果

TensorFlow.jsの処々の情報は可視化ライブラリであるTF-VISを使って表やグラフで可視化することができます。下記は上記のコードを実行した際に表示された情報です。

2.1. 学習データのプロット

学習データの「馬力(横軸:Horsepower)」と「燃費(縦軸:Miles Per Gallon)」の関係をプロットしたプロットチャートです。これを見ると、学習すべき馬力と燃費の関係は互いに反比例する関係にあることが伺えますが、直線的なの反比例関係ではなく、穏やかなカーブを描くような反比例であることが伺えます。

Horsepower vs Miles Per Gallon.png

2.2. 学習の進行状況

下記は損失関数の値です。損失関数をmse(= Mean Squared Error = 平均二乗誤差)に指定し、学習のエポックごとにその値が降下していっていることがわかります。このコードでは学習を50エポックまでと指定したので横軸は50までとなっています。損失関数の値がこれで十分なのかどうかと言う話は別途に言及します。

Training Performance - mse.png

2.3. 学習したモデルによる出力予測

学習させたモデルに入力として「馬力」の値を与え、その出力として「予測(predict)される燃費」を得ました。その関係を先ほどの学習データのプロットチャートに重ねて表示したものがこれです。学習したモデルが出力したオレンジ色の点が直線的に並んでいます。

Model Predictions vs Original Data.png

3. 次回の話

TensorFlow.orgのチュートリアルは上記で終わりとなっているのですが、次回は上記で記述したコードと作成したモデルについて、「これで良いのでしたっけ?」と言う視点で考察と改造を行います。

と言うのは、学習データの「馬力(横軸:Horsepower)」と「燃費(縦軸:Miles Per Gallon)」の関係は直線的なの反比例関係ではなく、穏やかなカーブを描くような反比例であることが伺えます。しかしながら、学習させたモデルが予測した関係は直線的な正比例の傾向を示しています。もっと、正確に入力と出力の関係を学習させるにはどうすればイイのか?次回の投稿で考えていきます。


追伸: Machine Learning Tokyoと言うMachine Learningの日本最大のグループに参加しています。作業系の少人数会合を中心に顔を出しています。基本的に英語でのコミュニケーションとなっていますが、能力的にも人間的にもトップレベルの素晴らしい方々が参加されておられるので、機会がありましたら参加されることをオススメします。

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

Magic Leap MagicScript Landscape Application. UiTextEdit

Prepare
Magic Leap One
https://www.magicleap.com/magic-leap-one

mlsdk v.0.20.0
https://creator.magicleap.com/downloads/lumin-sdk/overview

magic-script-cli v2.0.1
https://www.npmjs.com/package/magic-script-cli

magic-script-polyfills v2.1.1
https://www.npmjs.com/package/magic-script-polyfills

Create Project

magic-script init my-input org.magicscript.textinput "Text Input"
cd my-input

Code
Change app.js

import { LandscapeApp, ui } from 'lumin';
const { 
    UiLinearLayout
  , UiText
  , UiTextEdit
  , Alignment
  , HorizontalTextAlignment
  , EclipseLabelType } = ui;

export class App extends LandscapeApp {
  onAppStart () {

    const prism = this.requestNewPrism([1.0, 0.5, 0.5]);
    const layout = UiLinearLayout.Create(prism);
    layout.setAlignment(Alignment.CENTER_CENTER);

    const input = UiTextEdit.Create(prism, "", 0.4, 0.05);
    input.setAlignment(Alignment.CENTER_CENTER);
    layout.addItem(input, [0.005, 0.005, 0.005, 0.005]);

    const text = UiText.CreateEclipseLabel(prism, ""
                               , EclipseLabelType.kT7);
    text.setAlignment(Alignment.CENTER_CENTER);
    layout.addItem(text, [0.005, 0.005, 0.005, 0.005]);

    input.onTextChangedSub(function(uiEventData) {
      text.setText(input.getText());
    });
    prism.getRootNode().addChild(layout);
  }
}

Build

magic-script build -i

Run

magic-script run --port=10000

Reference
UiTextEdit (MagicScript API)
https://docs.magicscript.org/lumin.ui.UiTextEdit.html

Text Entry (UiTextEdit) (Guide C++)
https://creator.magicleap.com/learn/guides/luminrt-uitextedit

magicscript
https://www.magicscript.org/

Thanks!

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

MagicScript Landscape Application. UiTextEdit

Prepare
Magic Leap One
https://www.magicleap.com/magic-leap-one

mlsdk v.0.20.0
https://creator.magicleap.com/downloads/lumin-sdk/overview

magic-script-cli v2.0.1
https://www.npmjs.com/package/magic-script-cli

magic-script-polyfills v2.1.1
https://www.npmjs.com/package/magic-script-polyfills

Create Project

magic-script init my-input org.magicscript.textinput "Text Input"
cd my-input

Code
Change app.js

import { LandscapeApp, ui } from 'lumin';
const { 
    UiLinearLayout
  , UiText
  , UiTextEdit
  , Alignment
  , HorizontalTextAlignment
  , EclipseLabelType } = ui;

export class App extends LandscapeApp {
  onAppStart () {

    const prism = this.requestNewPrism([1.0, 0.5, 0.5]);
    const layout = UiLinearLayout.Create(prism);
    layout.setAlignment(Alignment.CENTER_CENTER);

    const input = UiTextEdit.Create(prism, "", 0.4, 0.05);
    input.setAlignment(Alignment.CENTER_CENTER);
    layout.addItem(input, [0.005, 0.005, 0.005, 0.005]);

    const text = UiText.CreateEclipseLabel(prism, ""
                               , EclipseLabelType.kT7);
    text.setAlignment(Alignment.CENTER_CENTER);
    layout.addItem(text, [0.005, 0.005, 0.005, 0.005]);

    input.onTextChangedSub(function(uiEventData) {
      text.setText(input.getText());
    });
    prism.getRootNode().addChild(layout);
  }
}

Build

magic-script build -i

Run

magic-script run --port=10000

Reference
UiTextEdit (MagicScript API)
https://docs.magicscript.org/lumin.ui.UiTextEdit.html

Text Entry (UiTextEdit) (Guide C++)
https://creator.magicleap.com/learn/guides/luminrt-uitextedit

magicscript
https://www.magicscript.org/

Thanks!

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

Vue.jsとaxiosでAPIからデータを取得して表示する

この記事でやること

静的なHTMLから検索条件を指定してAPIに接続してデータを取得、表示します。記事では手っ取り早くローカルPCに保存したHTMLからAPIに接続します。

接続するAPI

ユーザIDを指定して問い合わせると、ユーザ名とタイムスタンプをJSONで返してくるAPIをAWS上に用意しました。
以前書いたこっちの記事
Raspberry PiとPaSoRiでFelicaのNFCタグを読んでみる
の続きで、タッチ情報をIFTTTの代わりにAWS DynamoDBに蓄積するように変更。ユーザ情報を引いて名前と直近のタッチ時間をRambdaとAPI Gateway経由で取得できるようにしています。
こちらについては改めて記事にする予定です。

HTMLのソース

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>NFC demo</title>
  </head>
  <body>
    <div id="app">
        <input v-model="userid" placeholder="User ID">
        <button v-on:click="query">Query</button><br>
        <dl v-if="info">
          <dt>ID</dt><dd>{{info.id}}</dd>
          <dt>Name</dt><dd>{{info.name}}</dd>
          <dt>Date</dt><dd>{{info.timestamp|dateformat}}</dd>
        </dl>
        <div v-else="info">No touch data.</div>
    </div>
    <script src="https://unpkg.com/vue/dist/vue.min.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="app.js"></script>
  </body>
</html>

JavaScriptのソース

app.js
const apiUrl = "https://********.********.amazonaws.com/default/nfcDemoGetTouchTime"
const apiKey = "{APIキー}"
const config = {headers: {
    'Content-Type': 'application/json',
    'x-api-key': apiKey
}}

Vue.filter('dateformat', function(value){
    if (!value) return ''
    var zp = function(num){
        return (num < 10) ? '0'+ num: num
    }
    var dt = new Date(value * 1000)
    var dtstr = dt.getFullYear() + '-' + zp(dt.getMonth() + 1) + '-' + zp(dt.getDate()) + ' '
                + zp(dt.getHours()) + ':' + zp(dt.getMinutes())
    return dtstr
})

var app = new Vue({
    el: "#app",
    data:{
        info: null,
        userid: null
    },
    methods:{
        query: function(event){
            var querydata = {'id': this.userid}
            axios
            .post(apiUrl, querydata, config)
            .then(response => {
                this.info = response.data
                console.log(this.info)
            })
            .catch(error => console.log(error))
        }
    }
})

動かしてみる

index.htmlとapp.jsを適当なフォルダに保存して、ブラウザからindex.htmlを開きます。
初期画面.png

ユーザIDをフォームに入力してQueryボタンを押すと...
APIからJSONで問い合わせ結果が返ってきます。

{"id": "test1", "name": "\u30c6\u30b9\u30c8\u30e6\u30fc\u30b6\u30fc\uff11", "timestamp": 1557545262.413307}

実行画面.png
問い合わせ結果が画面に反映されました。

参考にしたドキュメント

Vue.jsは日本語版の公式ドキュメントが充実しているのでいろいろ助かります。
axios を利用した API の使用
フィルター — Vue.js

残課題

APIキーがソースから丸見えなので、外部にホスティングするときは、Cognitoで認証するとかアクセス制御の仕組みが必要になります。

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

Typescriptでreduxのmiddlewareを書く

Typescriptでreduxのmiddlewareを書こうと思ったら型がよくわからなくなったので、覚書。
この書き方だと古いのかうまく行かなかったので。

結論

reduxのindex.d.tsを読めばわかることなのですが...

import { Middleware, MiddlewareAPI, Dispatch, AnyAction } from 'redux';

export const timerMiddleware: Middleware = 
  <S extends AppStore>({ getState }: MiddlewareAPI<Dispatch, S>) => 
    (next: Dispatch<AnyAction>) => 
      (action: any): any => {
        /* middlewareの処理 */
        return next(action);
      };

参考

TypeScriptでReduxミドルウェアを作る - @IzumiSy

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

Vue.js + Firebaseで自己紹介用の静的サイトを作ってみた 第2弾 実装編(ルーティング)

はじめに

Vue.js + Firebaseで自己紹介用の静的サイトを作ってみた 第1弾 環境構築編の続きとなります。

要件

今回は以下画面構成と機能で簡単な静的サイトを実装していきます。
○画面構成
・トップページ(Home)
・プロフィールページ(About)
・スキルページ(Skill)
・お問い合わせページ(Contact)
○機能
・各ページへのルーティング(ヘッダー、フッターにナビゲーション配置)
・SNS(Twitter, Instagram, GitHub)ページへのリンク(フッターにSNSアイコン配置)
・お問い合わせフォームからお問い合わせ。(名前、Eメール、内容入力フォームと必須入力)

実装

ルーティング

まずは各画面構成のページを作っていって各ページへのルーティング機能を実装していきたいと思います。
※vue add routerは第1弾 環境構築編で追加し忘れたプラグインなのでここで追加していますが、vue create時にdefaultでなくmanualで設定して追加する方法もあります。

$ vue add router
? Use history mode for router? (Requires proper server setup for index fallback 
in production) Yes

?  Invoking generator for core:router...
?  Installing additional dependencies...

removed 1 package and audited 23924 packages in 8.956s
found 0 vulnerabilities

✔  Successfully invoked generator for plugin: core:router
   The following files have been updated / added:

     src/router.js
     src/views/About.vue
     src/views/Home.vue
     package-lock.json
     package.json
     src/App.vue
     src/main.js

   You should review these changes with git diff and commit them.

そうするといくつかいくつかファイルが追加されてたり変更されてたりすることがterminalの内容から確認できます。
この時のファイル差分を見たい場合はgit diffで見れるので気になる方は見てみてください。

では少しApp.vueの差分を見てみます。

src/App.vue
 <template>
   <div id="app">
-    <img alt="Vue logo" src="./assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
+    <div id="nav">
+      <router-link to="/">Home</router-link> |
+      <router-link to="/about">About</router-link>
+    </div>
+    <router-view/>
   </div>
 </template>

ナビゲーションが追加されているのが分かると思います。
画面の方も見てみます。
スクリーンショット 2019-05-12 午後0.03.29.png
前回の環境構築編の時の画面からロゴの上にHOME|ABOUTといったリンクが追加されていることが確認できます。
実際にABOUTをクリックするとABOUTページへ遷移、HOMEをクリックすると元の画面に戻るはずです。
これを踏まえて先ほど要件で決めた画面構成で各ページに遷移できるよう設定をしていきます。

共通表示部分

src/App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/skill">Skill</router-link> |
      <router-link to="/contact">Contact</router-link>
    </div>
    <router-view/>
  </div>
</template>

ルーティング部分

src/router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import(/* webpackChunkName: "home" */ './components/Home.vue')
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './components/About.vue')
    },
    {
      path: '/skill',
      name: 'skill',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './components/Skill.vue')
    },
    {
      path: '/contact',
      name: 'contact',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './components/Contact.vue')
    }
  ]
})

トップページ(Home)

src/components/Home.vue
<template>
  <div class="home">
    <p>HOME</p>
  </div>
</template>

<script>
// @ is an alias to /src
import Home from '@/components/Home.vue'

export default {
  name: 'home',
  components: {
    Home
  }
}
</script>

プロフィールページ(About)

src/components/About.vue
<template>
  <div class="about">
    <p>ABOUT</p>
  </div>
</template>

<script>
// @ is an alias to /src
import About from '@/components/About.vue'

export default {
  name: 'about',
  components: {
    About
  }
}
</script>

スキルページ(Skill)

src/components/Skill.vue
<template>
  <div class="skill">
    <p>SKILL</p>
  </div>
</template>

<script>
// @ is an alias to /src
import Skill from '@/components/Skill.vue'

export default {
  name: 'skill',
  components: {
    Skill
  }
}
</script>

お問い合わせページ(Contact)

src/components/Contact.vue
<template>
  <div class="contact">
    <p>CONTACT</p>
  </div>
</template>

<script>
// @ is an alias to /src
import Contact from '@/components/Contact.vue'

export default {
  name: 'contact',
  components: {
    Contact
  }
}
</script>

これでルーティングの実装は終わりです。
画面の方も見てみます。
スクリーンショット 2019-05-12 午後0.59.38.png
こんなページになると思います。
実際に動かしてみるとAboutをクリックしたらAboutのページにSkillをクリックしたらSkillのページにContactをクリックしたらContactのページにそれぞれ各画面から遷移できることが確認できたと思います。
今回は、各ページ共通で扱いたい部分(今回で言うとナビゲーションの部分)はApp.vueに書いてます。
このように共通化できるものは共通化して書くことをオススメです。

まとめ

今回の対応で以下画面ページの作成と各ページへのルーティングの設定を行いました。(ナビゲーション配置)
・トップページ(Home)
・プロフィールページ(About)
・スキルページ(Skill)
・お問い合わせページ(Contact)

次回は今回作ったナビゲーションをヘッダーとフッターに設置する実装とフッターにSNSページへのリンクを設置する実装を行いたいと思います。
※実際に実装しながら備忘録として手順やポイントを記事に残している手前、もしかしたら今後他の実装をやっていく上で今までの実装を変える恐れもあるのでご容赦ください。

○リポジトリ
https://github.com/stkzk3110/vue_firebase-project
今回のコミット部分
Add Routing and Create Pages

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

TensorFlow.jsでDeepLearning(初めの整理)

1. 初めに

こんにちわ。Electric Blue Industries Ltd.という、ITで美を追求するファンキーでマニアックなIT企業のマッツと申します。このところ投稿が疎かになったので、何かお役に立てることをと思い、TensorFlow.jsの解説を不定期することにしました。

Deep Learningをおこなううえで最もメジャーな方法のひとつが「仮想環境やコンテナを作ってTensorFlow & Python」かと思います。Google Colabと言う素晴らしい環境もありますが、「はじめの一歩」にはまだまだ敷居が高いところがあり、初学者には気軽に取り組める状況ではないように思います。

そんな中で特に初学者にオススメしたいのが「TensorFlow.js」です。TensorFlow.jsはJavaScriptでDeep Learningをおこなうためのライブラリです。機能的にはPython版のTensorFlowと基本的に足並みを揃えています(TFGANのような目新しい関数はTensorFlow.jsにはなかったりしますが)。学習に際しての敷居が低く、教育現場での利用にも有効と認識しています。メリット・デメリットは下記かと思います。

メリット

  • Webブラウザがあれば即に動作させられます
  • Webページを作成する際に慣れ親しんだ人も多いJavaScriptでの記述
  • PythonとJavaScriptは記述に類似性があるので思考的に同時並行が楽
  • GPUの使用もブラウザのWebGL経由で行える
  • ウェブサイトへの組み込みが容易でウェブコンテンツ化がしやすい
  • デバイスのカメラやマイクとの連動が簡単
  • TensorBoardに相当するような可視化ライブラリ「TF-VIS」もあり視覚化できる
  • Pythonとの相互変換(コンバーター)も使用可能
  • モデルのインポートとエクスポートも可能

デメリット

  • 日進月歩のDeepLearning業界の進化ペースに微妙にキャッチアップし遅れている
  • 大規模な学習処理にはやっぱりちょっと重いかも

2. この投稿の目的

今後に複数回の投稿に分けて、TensorFlow.orgから公開されているTensorFlow.jsを用いたチュートリアルを、非常に基本的な事項についてまで細かく詳細に説明し、何をしているのか流れを理解するお手伝いをします。狙いとしては、どのようなデータ(配列・オブジェクト・テンソル等)がどのように処理されて何が行われるのかを理解できるようになることです。

3. Deep Learningで頻出するJavaScriptのおさらい

次回からの各チュートリアルの解説をする前に、ウェブサイトでのJavaScriptではあまり登場しない処理について言及します。これらを理解していることが解説を読む上での前提となるので、不明事項はあらかじめ学習をお願いしたいです。

3.1. 変数と宣言(var, let, const)

JavaScriptでは変数を用いる際には使い始めに変数を宣言してから用います。宣言のタイプにはいくつかあり、ウェブサイト内で記述する場合はほぼvarしか使いませんが、Deep Learningではletやconstが多用されます。

var : 通常の変数宣言(上書き可能)
let : ifやforといったブロックスコープ内でのみ有効(上書き可能)
const : ifやforといったブロックスコープ内でのみ有効(上書き不可能)

3.2. 非同期処理(async, await)

asyncは指定された処理を非同期で行うことを宣言し、awaitはasyncで指定された処理が実行結果を返す(コールバック)するまで待つことを宣言します。これもウェブサイト作成では滅多に出てこないものです。

3.3. データのマッピング(map)

JavaScriptのmapは配列データに使うメソッドであり、配列データの各要素1つずつに対し指定されたコールバック処理を実行し、その結果を新しい配列として返すことが出来ます(見かけからはピンと来ないが繰り返し処理をしている状況)。なお、mapと類似するメソッドにfilterとreduceがあるが、ここでは割愛する。

map_1.js
// 1から5までの5つの整数を格納した、要素が5つの配列をitemsと言う名称で作成。
const items = [1, 2, 3, 4, 5];

// 上記の配列itemsの各要素について、各配列要素をvalueとしてfunctionで規定された処理をおこなう。
const result = items.map(function(value) {

    //配列の各要素を2倍
    return value*2;

});

// ブラウザの「開発者ツール(inspect)」のconsole画面にresultの内容が出力されます。
console.log(result);

// resultは配列となり、下記の要素が格納される
// [2, 4, 6, 8, 10]

さらに、インデックス(配列の先頭要素(0番)から何番目か)の番号も使って下記のようにオブジェクトを要素に持たせることもでき、この形式はTensorFlow.jsでのデータ処理に頻出するものです。

map_2.js
const items = [1, 2, 3, 4, 5];

const result = items.map(function(value, index) {

    //valueとindexを含むオブジェクトを要素とする配列を生成
    return {
        value,
        index
    };

});

console.log(result);

// resultは配列となり、下記のvalueとindexを含むオブジェクトを各要素が格納される
// [{value: 1, index: 0}, {value: 2, index: 1}, {value: 3, index: 2}, {value: 4, index: 3}, {value: 5, index: 4}]

3.4 配列の生成(Array.from)

Array.from(x)は下記のように「配列っぽい(インデックス、要素、要素数が記録された)」オブジェクトから正真正銘の配列を生成するメソッドです。

arrayFrom_1.js
const items = {0: 11, 1: 22, 2: 33, length: 3};

const result = Array.from(items);

console.log(result);

// resultには下記の配列が格納される
// [11, 22, 33]

配列の要素にオブジェクトを格納させるべく、上記で述べたmapと併用して用いる場合は

arrayFrom_2.js
const items = {0: 11, 1: 22, 2: 33, length: 3};

const result = Array.from(items).map((value, index) => {
    return {
        x: value,
        y: index
    }
});

console.log(result);

// resultには下記のオブジェクトを要素とする配列が格納される
// [{x: 11, y: 0}, {x: 22, y: 1}, {x: 33, y: 2}]

のように記述します。この処理はDeep Learningの学習データとテストデータの生成に頻出する処理です。

さらに、オブジェクトの中にオブジェクトが入ったような形式の配列もどきの場合は、配列もどきオブジェクトの各要素に対して処理を行わせるべくmapと合わせると下記のようにハンドルできます。

arrayFrom_3.js
const items = {0: {alpha: 11, bravo:111}, 1: {alpha: 22, bravo: 222} , 2: {alpha: 33, bravo: 333}, length: 3};

const result = Array.from(items).map(d => {
    return {
        x: d.alpha,
        y: d.bravo
    }
});

console.log(result);

// resultは下記のオブジェクトを要素とする配列になる。
// [{x: 11, y: 111}, {x: 22, y: 222}, {x: 33, y: 333}]

Deep Learningの学習で最も難しく感じるのは、このように「配列」「オブジェクト」が入れ子になり、かつ多次元行列の要素となってデータが扱われるところかと思います。データを処理する際には「このデータはどのような構造になっているか」を確認しながら処理を理解することが、その後の汎用的なDeep Learning実施に肝要です。

4. TensorFlow.jsで行列を扱う

4.1. ブラウザでTensorFlow.jsが動くようにする

ブラウザ(Google Chromeの最新版を推奨)でTensorFlow.jsが動くようにするには、下記のHTMLファイルを作成し、それと同じフォルダに置いた script.js というJavaScriptを書いたファイルにJaveScriptでコードを記述することで行います(もちろんファイル名は別の名称でも良いです)。実行結果は「開発者ツール(inspect)」のコンソール画面に表示されます。

index.html
<!DOCTYPE html>
<html>
<head>
  <title>TensorFlow.js Tutorial</title>
    <!-- TensorFlow.jsのインポート -->
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.js"></script>
    <!-- JavaScriptを書くscript.jsのインポート -->
    <script type="text/javascript" src="script.js"></script>
</head>
<body></body>
</html>

4.2. 行列を作る

下記の方法1から3は全て同じ

a =
\begin{bmatrix}
1 & 2 \\
3 & 4 \\
5 & 6
\end{bmatrix}

という行列aを作ります。

方法1:各行ごとにまとめて指定することを機械的に繰り返す。

cretaeMatrix_1.js
const a = tf.tensor([[1, 2], [3, 4], [5, 6]]);

方法2:行列の要素を左上から機械的に続けて指定し、行列の形(m行 x n列)を指定する。

cretaeMatrix_2.js
const shape = [3, 2];
const a = tf.tensor([1, 2, 3, 4, 5, 6], shape);

方法3:方法1と方法2の合わせ技(なお、画像や音声の学習ではデータが浮動小数になるのでint32などと明示的に指定することも多いので、これは意外と使います)。

cretaeMatrix_3.js
const a = tf.tensor([[1, 2], [3, 4], [5, 6]], [3, 2], 'int32');

4.3. 行列の変形

  • reshape: 上記で作成した行列aに対して、shapeでTensorの再形成を行い行列bを生成
reshape_1.js
// 2行3列に変形
const b = a.reshape([2, 3]);
b =
\begin{bmatrix}
1 & 2 & 3 \\
4 & 5 & 6
\end{bmatrix}
  • transpose: 上記で作成した行列aに対して、transposeで行列の行と列を転置して転置行列bを生成する
transpose_1.js
// 行と列を対角線を軸としてくるっとひっくり返し(転置)
const b = a.transpose();
b =
\begin{bmatrix}
1 & 3 & 5 \\
2 & 4 & 6
\end{bmatrix}

4.4. 行列の要素を得る

  • arraySyncで行列aの各行を要素とする配列を得る
arraySync_1.js
b = a.arraySync();
b =
\begin{bmatrix}
\begin{bmatrix}
1 & 2 
\end{bmatrix} , 
\begin{bmatrix}
3 & 4 
\end{bmatrix} , 
\begin{bmatrix}
5 & 6 
\end{bmatrix}
\end{bmatrix}
  • dataSyncで行列aの各値を要素とする配列を得る
dataSync_1.js
b = a.dataSync();
b =
\begin{bmatrix}
1, 2, 3, 4, 5, 6 
\end{bmatrix}

4.5. 行列の算術計算

  • 行列xの各要素の二乗を得る
square_1.js
const x = tf.tensor([1, 2, 3, 4]);
const y = x.square();
y =
\begin{bmatrix}
1, 4, 9, 16 
\end{bmatrix}

ということで、今後のTensorFlow.jsの詳細解説を前に、今回はベースとなるJavaScriptのおさらいまでをしました。空いた時間をうまく使って、シュウイチくらいでは続きを投稿していく予定です。どうぞよろしくお願いします。

追伸: Machine Learning Tokyoと言うMachine Learningの日本最大のグループに参加しています。作業系の少人数会合を中心に顔を出しています。基本的に英語でのコミュニケーションとなっていますが、能力的にも人間的にもトップレベルの素晴らしい方々が参加されておられるので、機会がありましたら参加されることをオススメします。

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

Carbonの初期画面のソースコードをひもとく

はじめに

Carbon というWebサービスをご存じでしょうか?

Carbon: https://carbon.now.sh/

ソースコードをシンタックスハイライト付きで画像化してくれる、ツイッター投稿などに便利なWebサービスです。この Carbon なのですが、初期画面には短い javascript のコードが表示されます。

const pluckDeep = key => obj => key.split(".").reduce((accum, key) => accum[key], obj);

const compose = (...fns) => res => fns.reduce((accum, next) => next(accum), res);

const unfold = (f, seed) => {
  const go = (f, seed, acc) => {
    const res = f(seed);
    return res ? go(f, res[1], acc.concat([res[0]])) : acc;
  };
  return go(f, seed, []);
};

このコードは一体何を表しているのでしょうか?

アロー関数が連続していたりと javascript では普段見慣れない構文が並んでいたので、順番にひもといてみると、関数型プログラミングというキーワードが浮かび上がってきました。

おことわり

当方勉強中につき、関数型プログラミング自体の細かい解説は避けます。

前準備

これらの関数をひもとくには、2つの知識が必要です。

  1. 関数を返す関数
  2. Array.reduce() の第2引数

関数を返す関数

javascript では関数もオブジェクトであるため、通常の値と同様に、引数で渡したり返り値で返したりできます。

このうち、関数を返す関数の動きを追ってみましょう。

const add = a => (b => a + b);

const add2 = add(2);
// add2 = b => 2 + b
const result = add2(3); // 2 + 3 = 5

add は引数 a を受け取ると関数 b => a + b を返す関数です。
add(2) は引数 2 を受け取ったので、b => 2 + b を返しました。
add2 は引数 b を受け取ると 2 + b を返す関数になります。

()とadd2を省略して、下のように書けます。

// in short
const add = a => b => a + b;
const result = add(2)(3); // 5

アロー関数が連なっている表記のナゾは解けましたね。
この書き方の利点などはさておき、仕組みは理解されたでしょうか?

補足: 「カリー化された関数」で調べると関数型プログラミングのディープな世界に足を突っ込めます

Array.reduce() の第2引数

Array.reduce() は、第1引数に関数 (reducer) を渡すことで、配列からひとつの値を作り出す関数です。

reducer の第1引数にはひとつ前の reducer の出力が、第2引数には配列の値が順々に渡されます。

// accum - accumulate: 蓄積
const result = [2, 3, 4].reduce((accum, next) => accum + next); // (2 + 3) + 4

この Array.reduce() は第2引数として、reducer の初期値を与えることができます。

Array.prototype.reduce() - JavaScript | MDN

const result = [2, 3, 4].reduce((accum, next) => accum + next, 30);
// ((30 + 2) + 3) + 4

Array.reduce() は使いこなすと、map も filter もなんでもこなす、すごいやつなのですが、それはまた別の話。

// mapの代わり
const result = [3, 4, 5].reduce((accum, next) => [...accum, next + 1], []);
// [4, 5, 6]

関数をひもとく

それでは実際に関数をひもといてみましょう。

pluckDeep

const pluckDeep = key => obj => key.split(".").reduce((accum, key) => accum[key], obj);
// .reduce の第2引数: obj
// .reduce の返り値: obj[key][key][key]...

実際の使用例を見て流れを追ってみましょう。

// How to use
const pluckFooBar = pluckDeep('foo.bar');
const result = pluckFooBar({ foo: { bar: 'result!' } });
// key = 'foo.bar'
// key.split(".") = ['foo', 'bar']
// pluckFooBar = obj => (obj[foo])[bar]
// obj = { foo: { bar: 'result!' } }
// result = 'result!'

オブジェクトのプロパティにアクセスする関数だとわかります。

compose

const compose = (...fns) => res => fns.reduce((accum, next) => next(accum), res);
// .reduce の第2引数: res
// .reduce の返り値: next(next(next(res)))...

関数を合成する関数だとわかります。

// How to use
// Sample from https://ramdajs.com/docs/#compose
const calc = compose(Math.abs, a => a + 1, b => b * 2);
const result = calc(-4); // 10
// fns = [Math.abs, a => a + 1, b => b * 2]
// calc = res => ((Math.abs(res)) + 1) * 2
// res = -4
// result = ((Math.abs(-4)) + 1) * 2

上が使用例です。複数の関数を合成して、後から値を与えているのがわかりますね。

unfold

const unfold = (f, seed) => {
  const go = (f, seed, acc) => {
    const res = f(seed);
    return res ? go(f, res[1], acc.concat([res[0]])) : acc;
  };
  return go(f, seed, []);
};
// f(seed) の返り値: [value, nextSeed] or false
// go() の返り値: [value, value, value, ...]

簡単な再帰関数だとはわかります。
しかしこのままでは用途が分からないですね。使用例を出してみましょう。

// How to use
// Sample from https://ramdajs.com/docs/#unfold
const f = n => n > 50 ? false : [-n, n + 10];
const seed = 10;
const result = unfold(f, seed);
// [-10, -20, -30, -40, -50]

シード値と関数からリストを生成する関数だとわかります。

最後に

カリー化, reduce から始まって、pluck, compose, unfold と、実はどれも関数型プログラミングの道具たち。ES6になってアロー関数が導入されたことで、とてもシンプルに書けるようになりました。初期画面のコードは、ES6で関数型プログラミングを始めませんか?という開発者からのメッセージなのでしょう。

関数型プログラミングが気になった方へ

関数型プログラミングについてはまだ知らないことだらけなので、詳細の紹介はできないのが心苦しいところです。代わりにいくつか検索キーワードを載せておきます。

  • Ramda.js、Underscore.js、lodash.js
  • Haskell
  • ラムダ式
  • カリー化、部分適用、遅延評価
  • 副作用
  • 純粋関数型データ構造
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[RPGツクールMV]データベースでアクターの名前を変えてもセーブデータには反映されない

概要

RPGツクールMVのデータベースとセーブデータに関する話

例えばアップデートでキャラクタの名前を「ハロルド」から「アレックス」に変更したい!
となった場合、データベースの名前を「アレックス」に変えても、既にプレイしている人のセーブデータから読み込むと「ハロルド」のままになってしまいます

RPGアツマールなどに投稿して継続してアップデートしていくようなゲームの場合、これだと困る場合があります

なぜか

  • データベースに入力したアクターの名前はGame_Actor_nameに入る
  • セーブデータにはGame_Actorがまるごと保存される
  • 続きから遊ぶ場合、Game_Actorにはセーブデータの情報が入る

イベントから名前を変更できるようにするためと思いますが、ゲーム中に名前を変える必要がない場合にはセーブデータで持っていない方が都合が良いです

実装を変更する

プラグインとして実装して以下のように挙動を変更します

(function() {

// ===============================================================
// 名前(name)は$dataActorsを参照して自身で持たないように
// ※ゲーム中での書き換えはできないので注意
// ===============================================================

// ---------------------------------------------------------------
// internal定義
// ---------------------------------------------------------------

var internal = {}
// 不要な情報を削除
internal.lightenGameActor_ = function(actor)
{
    delete this._name
}

// ---------------------------------------------------------------
// Game_Actor上書き
// ---------------------------------------------------------------

// Game_Actorで持たない情報を削除
var _GameActor_initMembers = Game_Actor.prototype.initMembers
Game_Actor.prototype.initMembers = function() {
    _GameActor_initMembers.call(this)
    internal.lightenGameActor_(this)
};

var _GameActor_setup = Game_Actor.prototype.setup
Game_Actor.prototype.setup = function(actorId) {
    _GameActor_setup.call(this,actorId)
    internal.lightenGameActor_(this)
};

// name
Game_Actor.prototype.name = function() {
    return $dataActors[this.actorId()].name;
};

Game_Actor.prototype.setName = function(name) {
    // 何もしない
};

})();
  • 元々メンバで持っていたものを削除する
  • name()$dataActorsから取得するようにする
    • データベースで入力したものが参照されます
  • setName()では何もしないように変更する

名前_name 以外に下記のメンバに対しても同様の実装を入れておくと、
これらもセーブデータに入ったものでなくデータベースの方を参照するようになります

  • プロフィール _profile
  • 2つ名 _nickname
  • 歩行キャラ _characterName _characterIndex
  • _faceName _characterIndex
  • [SV]戦闘キャラ _battlerName

注意点

  • Game_Actor.prototype.setName で何もしなくなるため、イベントから「名前の変更」を行えなくなります
    • 「名前」以外のメンバに対しても同様の実装を入れると、これらもイベントからの変更ができなくなります

おわりに

継続してアップデートするゲームというのも多くなってきていると思います。その時に、プロフィールにちょっと誤字があった…とかの不具合を治そうとすると、ゲームからはデータベースを参照してもらう形の方が良かったりします
ただしそれらを変更するイベントのコマンドは使えなくなりますので、どちらの形がよいかはゲームの仕様次第になると思います

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

配列を仮想DBとした掲示板の作成(5/5):DELETEリクエスト編

GitHub

https://github.com/atlansien/js_excercise_for_backend_7

前提条件

配列を仮想DBとした掲示板APIサーバの作成(1/5):はじめに・環境構築・DB、コメントモデルの作成

配列を仮想DBとした掲示板の作成(2/5):GETリクエスト・サーバ起動編

配列を仮想DBとした掲示板の作成(3/5):POSTリクエスト編

配列を仮想DBとした掲示板の作成(4/5):PUTリクエスト編

実装する機能

「DELETE/api/comment/:id」リクエストを送ると、id値に紐づいたComment1件を削除して、削除したComment1件がレスポンス値として返ってくる

この記事のまとめ

  • 指定したidと合致したcomment1件を削除して、そのコメントを返り値とするメソッドを作成した
  • DELETEリクエストを送った時のメソッドを作成した
  • メソッド名の変更を行なった
  • POSTMANでDELETEリクエスト時の動きを確認した

指定したid値と合致したcomment1件を削除して、削除したコメント1件を返すメソッドの作成(deleteComment()

deleteComment: ({ id }) => {
  if (typeof id !== 'number' || id < 1) {
    throw new Error(
      'idに適切でない値が入っています、1以上の数字を入れてください'
    );
  }

  const target = comments.findIndex(comment => id === comment.id);
  if (target === -1) {
    throw new Error('idと合致するCommentが見つかりません');
  }
  const deletedComment = comments.splice(target, 1)[0];

  return deletedComment;
},
  • id値に適切でないプロパティ値が入っている
  • id値と合致するcommentが見つからない

上記の場合はエラーを返し、適切な値が入っている場合のみメソッドが成功するように設計しました。

また、idのプロパティ値と配列のインデックス値は同一ではないので、配列内からid値と合致する要素を探す必要があります、そこでArray.prototype.findIndex()を使用します。

const target = comments.findIndex(comment => id === comment.id);

アロー関数の特徴である、文の戻り値を返すだけならブロックとreturnを省略した書き方をしています。
これで、id値と合致するコメントを配列内から探すことができます。

そして、コメントを一件削除して、そのコメントを返す必要があります。

const deletedComment = comments.splice(target, 1)[0];

return deletedComment;

Array.prototype.splice()は破壊的なメソッドです、第一引数に先ほど会得したtarget、第二引数で削除したい配列の数1を入れることで、id値と合致するコメント1件がcomments配列から削除されます。

ここで、splice()の末尾に[0]をつけてますが、これをつけることで、配列ではなく1件のデータとして要素を返すことができます。

詳しい説明はこちらで記事にしてます。
【Javascript】splice()の末尾に[0]をつけ取り除かれた要素1つをデータで返す - じんのアウトプット日記(だいたい毎日更新)

それでは、実際にうまく動作するかテストします。

deleteComment()のテスト

テスト内容

  1. Comment.deleteCommentはメソッドである
  2. idの引数に不正な値が入っていた場合、エラーが返る
  3. idの引数と合致するCommentがない場合、エラーが返る
  4. 適切なデータを送った場合、idと合致するComment1件が返ってくる
  5. 4.と同時に、idと合致するComment1件が配列から削除される。

この5点を確認します。

deleteComment.test.js
const assert = require('power-assert');
const Comment = require('../../models/Comment');

describe('Comment.deleteCommentのテスト', () => {
  // 以下にテストコードを記述していきます
});

1.Comment.deleteCommentはメソッドである

it('Comment.deleteCommentはメソッドである', () => {
  assert.strictEqual(typeof Comment.deleteComment, 'function');
});

typeof演算子で、Comment.deleteCommentがメソッドであるかどうかテストします。

2.idの引数に不正な値が入っていた場合、エラーが返る

it('idの引数に不正な値が入っていた場合、エラーが返る', () => {
  const invalidIdList = [
    { id: 0 },
    { id: -1 },
    { id: null },
    { id: {} },
    { id: [] },
    { id: '1' },
  ];

  invalidIdList.forEach(id => {
    try {
      Comment.deleteComment(id);
      assert.fail();
    } catch (error) {
      assert.strictEqual(
        error.message,
        'idに適切でない値が入っています、1以上の数字を入れてください'
      );
    }
  });
});

invalidIdList配列に思いつく限りの適切でない値を入れ、forEachで各値ごとにdeleteCommentを実行します。メソッドがエラーになるようであればcatchの方でエラーメッセージと比較します。 1つでも成功した場合はassert.fail()によりエラーで終了、失敗となります。

3.idの引数と合致するCommentがない場合、エラーが返る

it('idのプロパティ値と合致するCommentがない場合、エラーが返る', () => {
  const invalidId = { id: 999999999 };

  try {
    Comment.deleteComment(invalidId);
    assert.fail();
  } catch (error) {
    assert.strictEqual(error.message, 'idと合致するCommentが見つかりません');
  }
});

invalidIdに現在配列には入っていないid値を入れ、deleteCommentを実行します。メソッドがエラーになるようであればcatchの方でエラーメッセージと比較します。成功した場合はassert.fail()によりエラーで終了、失敗となります。

4.適切なデータを送った場合、idと合致するComment1件が返ってくる

  it('適切なid値を送った場合、idと合致するComment一件が返される', () => {
    const validId = { id: 1 };

    const deletedComment = Comment.deleteComment(validId);
    assert.deepEqual(deletedComment, {
      id: validId.id,
      username: deletedComment.username,
      body: deletedComment.body,
      createdAt: deletedComment.createdAt,
      updatedAt: deletedComment.updatedAt,
    });
  });

idに適切な値が入っていた場合は、deleteCommentが実行した時に、返ってくる要素の各プロパティ値には適切にデータが入っているかassert.deepEqualで確認します。

5.4.と同時に、idと合致するComment1件が配列から削除される。

it('適切なid値を送った場合、idと合致するComment一件が配列から削除される', () => {
  const oldComments = Comment.findAll();
  const validId = { id: 2 };

  Comment.deleteComment(validId);

  const currentComments = Comment.findAll();

  assert.equal(oldComments.length, currentComments.length + 1);
});

idに適切な値が入っていた場合は、deleteComment実行後、配列から要素が一件削除されているかテストします。

各テストが成功したら、次はAPIからDELETEリクエストをした時に、deleteCommentが実行するようにしていきます。

「DELETE/api/comment/:id」リクエストを送ると、id値と合致したcomment1件を削除して、削除したcomment1件がレスポンス値として返ってくるメソッドの作成(deleteComment())

comment.js(controller)
deleteComment: (req, res) => {
  try {
    const parsedId = parseInt(req.params.id, 10);

    const removeComment = Comment.removeComment({
      id: parsedId
    });

    res.status(200).json(removeComment);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
}
comment.js(router)
router
  .route('/:id')
  .put(controller.putComment)
  .delete(controller.deleteComment);

今回もPUTリクエストと同じくルートパラメータを使用します。ルートパラメータで得た値を数値に変換し、removeCommentのid値に代入します。
と、その前に突然現れたremoveCommentとはなんでしょうか?

メソッド名の変更

今回DELETEリクエスト時に実行されるdeleteCommentと、指定したid値と合致したComment1件を削除して、削除したコメント1件を返すメソッドにて、modelsで作成したdeleteCommentの名前が被ってしまいましたので、 modelsの方をremoveCommentと改名しました。

deleteComment()のテスト

test/app/apiディレクトリにdeleteComment.test.jsファイルを新たに作成し、そちらでテストを行なっていきます。

deleteComment.test.js
const assert = require('power-assert');
const requestHelper = require('../../../helper/requestHelper');

const getComments = async () => {
  const response = await requestHelper.request({
    method: 'get',
    endPoint: '/api/comments',
    statusCode: 200,
  });
  return response.body;
};

const deleteComment = async (id, code) => {
  const response = await requestHelper.request({
    method: 'delete',
    endPoint: `/api/comments/${id}`,
    statusCode: code,
  });
  return response;
};

describe('TEST 「DELETE api/comments/:id」', () => {
  // 以下にテストコードを記述していきます
});

テスト内容

  1. 適切でないidを送ると400エラーが返る
  2. 送られたidとひもづくCommentがない場合、400エラーが返る
  3. 適切なデータを送った場合、idとひもづくCommentがレスポンス値として返ってくる。
  4. 3.と同時に、配列内にあったidとひもづくCommentは配列から削除される。

この4点を確認するテストコードを記述していきます。

1.適切でないidを送ると400エラーが返る

it('適切でないid値を送るとエラーが返る', async () => {
  const response = await deleteComment(0, 400);

  assert.deepStrictEqual(response.body, {
    message: 'idに適切でない値が入っています、1以上の数字を入れてください',
  });
});

id値に0(適切でない数値)を入れると、ステータスコードは400、そしてエラーメッセージが返ってくることを確認します。

2.送られたidとひもづくCommentがない場合、400エラーが返る

it('idの値と合致するCommentがない場合エラーが返る', async () => {
  const response = await deleteComment(9999999, 400);

  assert.deepStrictEqual(response.body, {
    message: 'idと合致するCommentが見つかりません',
  });
});

現在配列に入っているcommentのidのどれにも合致しない数字をidに入れると、ステータスコードは400、そしてエラーメッセージが返ってくることを確認します。

3.適切なデータを送った場合、idとひもづくcomment1件がレスポンス値として返ってくる。
4.3.と同時に、配列内にあったidとひもづくcomment1件は配列commentsから削除される。

it('適切なid値を送ると、idと合致するCommentが返ってくる、また該当のCommentは配列内から削除される', async () => {
  const oldComments = await getComments();

  const validId = 4;
  const response = await deleteComment(validId, 200);
  const comment = response.body;

  assert.deepStrictEqual(comment, {
    id: validId,
    username: comment.username,
    body: comment.body,
    createdAt: comment.createdAt,
    updatedAt: comment.updatedAt,
  });

  const currentComments = await getComments();

  assert.strictEqual(oldComments.length, currentComments.length + 1);
});

id値と合致するcommentがあった場合、合致したcomment1件、ステータスコード200が適切に返ってきていることを確認します。
それと同時に、deleteComment実行前と実行後の配列を比較して、comment1件が削除されていることを確認します。

以上のテストが全て正常に終了したら、今回のAPIサーバを用いた掲示板の作成は全て終了です。

最終的な環境

.
├── Task_info.md
├── app.js
├── controllers
│   └── comments.js
├── helper
│   └── requestHelper.js
├── index.js
├── models
│   └── Comment.js
├── package-lock.json
├── package.json
├── readme.md
├── routers
│   └── comments.js
└── test
    ├── app
    │   └── api
    │       ├── deleteComment.test.js
    │       ├── getComment.test.js
    │       ├── postComment.test.js
    │       └── putComment.test.js
    ├── mocha.opts
    └── models
        ├── createComment.test.js
        ├── findAll.test.js
        ├── removeComment.test.js
        └── updateComment.test.js

POSTMANを使用してDELETEリクエスト時の動きをチェックする(gif)

DELETErequest.gif

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

便利ページ:トレンドキーワードを取得してみた

前回までに、なにかと便利なページを作成しました。

 便利ページ:Javascriptでちょっとした便利な機能を作ってみた
 便利ページ:自分のQiita記事を一覧表示
 便利ページ:元号を変換してみた

今回は巷で噂のトレンドキーワードを取得するするページを追加しました。
とは言っても、以下の以前の投稿内容と同じで、お便利ページに移植しただけです。
 Dialogflowと連携してLINE Botを作る

毎度の通り、デモページとGitHubです。

GitHub
 https://github.com/poruruba/utilities

デモページ
 https://poruruba.github.io/utilities/

サーバ側の実装

トレンドキーワードを収集するために、「Twitter REST API」を使います。
Twitter Developerアカウントに登録されている必要があります。

swagger.yaml
  /trendword:
    post:
      x-swagger-router-controller: routing
      operationId: trendword
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: object
index.js
var fetch = require('node-fetch');
const { URLSearchParams } = require('url');

const TWITTER_API_KEY = process.env.TWITTER_API_KEY || TwitterアプリのAPI key;
const TWITTER_API_SECRET_KEY = process.env.TWITTER_API_SECRET_KEY || TwitterアプリのAPI secret key;
const YAHOO_WOEID = process.env.YAHOO_WOEID || 【トレンドを取得したい場所のWOEID; 
const NUM_OF_PICKUP = 10;

exports.handler = async (event, context, callback) => {
    if( event.path == '/trendword' ){
        var body = JSON.parse(event.body);
        if( body.apikey != SERVER_APIKEY )
            throw 'apikey mismatch';

        var trends = await get_trendlist();
        return new Response({ result: 'OK', trends: pickup_list(array_shuffle(trends.trends)) });
    }
};

function array_shuffle(array){
    for(var i = array.length - 1; i > 0; i--) {
        var j = Math.floor(Math.random() * (i + 1));
        var tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    return array;
}

function pickup_list(list){
    var pickup = [];
    for( var i = 0 ; i < NUM_OF_PICKUP ; i++ ){
        var name = list[i].name;
        if( name.slice(0, 1) == '#' || name.slice(0, 1) == '?' )
            name = name.slice(1);

        pickup.push(name);
    }

    return pickup;
}

function get_trendlist(){
    var body = {
        grant_type: 'client_credentials'
    };
    return do_post_secret('https://api.twitter.com/oauth2/token', body, TWITTER_API_KEY, TWITTER_API_SECRET_KEY)
    .then(result =>{
        var body = {
            id: YAHOO_WOEID
        };
        return do_get_token_text('https://api.twitter.com/1.1/trends/place.json', body, result.access_token);
    })
    .then(result =>{
        var list = JSON.parse(result);
//        console.log(list);

        return list[0];
    });
}

function do_post_secret(url, body, client_id, client_secret){
    var data = new URLSearchParams();
    for( var name in body )
        data.append(name, body[name]);

    var basic = new Buffer(client_id + ':' + client_secret).toString('base64');

    return fetch(url, {
        method : 'POST',
        body : data,
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Basic ' + basic }
    })
    .then((response) => {
        if( response.status != 200 )
            throw 'status is not 200';
        return response.json();
    });
}

function do_get_token_text(url, qs, token){
    var params = new URLSearchParams();
    for( var key in qs )
        params.set(key, qs[key] );

    console.log(url + '?' + params.toString());
    return fetch(url + '?' + params.toString(), {
        method : 'GET',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer ' + token }
    })
    .then((response) => {
        if( response.status != 200 )
            throw 'status is not 200';
        return response.text();
    });
}

以下の部分を環境に合わせて書き換えます。

【TwitterアプリのAPI key】
【TwitterアプリのAPI secret key】
【トレンドを取得したい場所のWOEID】

動作確認

ブラウザにはこんな感じに表示されます。

image.png

以上

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

初めてのJavaScript

JavaScriptとは

JavaScript(ジャバスクリプト)とは、プログラミング言語のひとつである。Javaと名前が似ているが、全く異なるプログラミング言語である。(Wikipedia参照)

Javaというプログラミング言語は大学の図書館で見たことがあり知っていたのですが、JavaScriptという言語は初耳でマイナーな言語だと思っていたのですが...
これがなんと今ではほぼ全てのWebサイトで使われているとかなんとか。

で、何ができるの???

JavaScriptは、主にWebサイトやWebアプリの開発によく使われます。Webサイトやアプリではフロントエンド、バックエンドで処理が行われます。フロントエンドは見た目部分、バックエンドは内部の処理などを担当します。JavaScriptは両方の処理に長けています。何はともあれコードを書いて実際に動きを見てみないとJavaScriptについて理解は深まらないでしょう。

実際に書いてみる

では実際に書いてみましょう。コードを書くためにはediterというコードを書く場所が必要です。自分はVisual Studio Codeを使っています。
Visual Studio Code - Code Editing. Redefined

Hello World を表示してみる。

sample.html
<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>初めてのJavascript</title>
 </head>
<body>
 <script>
   //ここにjavascriptを書く。
  window.alert("Hello World");
 </script>
</body>
</html>

説明していくとscriptタグで囲んでいる場所以外はHtmlの決まった書き方でscriptタグの中にJavaScriptのコードを書いていきます。
では、scriptタグの中をみてみましょう。window.alert("Hello World")と書かれています。
window.alertは()で指定された文字列をダイアログ表示するための命令です。そして()内に表示させたい文字列を書くのですが、文字列は""で囲わないといけません。
これでダイアログにHello World と表示できたかと思います。ちなみにscriptタグ外にJavascriptのコードはかけないので注意が必要です。

次回予告

次回の記事はJavaScriptの基本的な記法について書こうと思います。
それでは、また次回会いましょう。
サービス、サービス

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