20200214のJavaScriptに関する記事は14件です。

[Node.js][JavaScript]CryptoAPIの違いでハマったのでまとめ

Overview

Node.jsはJavaScriptで書けるから、Webの中では"Write once, run anywhere"的な美味しいこともある。
しかし、各環境にbuiltinされているAPIを使ったときはそうはいかない時がある。
今回は暗号化のCryptoで不覚にも1日ハマったのでその記録を残しておく。

Target reader

  • Node.jsで暗号化したデータをブラウザで復号化したいと思っている方。

Prerequisite

  • AESの概要は理解していること。
  • 今回はAES256-CBCを使用する。
    • 記憶が正しければAES192はブラウザのAPIでサポートされていない旨のエラーが出たため。

Body

どうして片方のAPIで統一しないの?

これはいい質問だ。実際のところ、Node.jsのcryptoをブラウザで実行したことがある。
どうして採用されなかったのか?なぜなら100KBほどバンドルサイズが増えたから。
詳しく知りたい場合は、この方の記事を読んでみるといいかもしれない。
https://engineering.mixmax.com/blog/requiring-node-builtins-with-webpack

一言でいうと、以下のブラウザ用cryptoがバンドルされてしまったため。
https://github.com/crypto-browserify/crypto-browserify

ブラウザのAPIを使えば100KBのバンドルを回避できるのだから、別々のAPIを使用するのは当然といってもいい。
もしかしたら差分を吸収するI/Fのパッケージがあるかもしれないが調べてない:joy:

Node.jsのCrypto

基本的には公式ドキュメントのコードがそのまま使用できる。
https://nodejs.org/api/crypto.html#crypto_class_cipher

大して見どころはないが、私のソースも載せておく。
ライブラリの方のソース。

nodeCrypto.js
import crypto from 'crypto';

function createCipheriv(algorithm, key, iv) {
    console.log("crypt.key:", key);
    console.log("crypt.iv:", iv);

    const cipher = crypto.createCipheriv(algorithm, key, iv);
    return cipher;
}

function createDecipheriv(algorithm, key, iv) {
    console.log("decrypt.key:", key);
    console.log("decrypt.iv:", iv);

    const decipher = crypto.createDecipheriv(algorithm, key, iv);
    return decipher;
}

async function cryptByNodeApi(cipher, plainText) {
    console.log('平文: ' + plainText);

    let encrypted = cipher.update(plainText, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    console.log('暗号化:', encrypted);

    return encrypted;
}

async function decryptByNodeApi(decipher, encrypted) {
    // 復号
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    console.log('復号化: ', decrypted);

    return decrypted;
}

export {
    createCipheriv, createDecipheriv,
    cryptByNodeApi, decryptByNodeApi
}

実行部分のソースの抜粋。

import { cryptByNodeApi, decryptByNodeApi, createCipheriv, createDecipheriv } from './libs/nodeCrypto';

export default function App() {

  async function handleClickNodeToBrowser() {
    const algorithm = 'aes-256-cbc';
    const key = crypto.randomBytes(32);
    const iv = Buffer.alloc(16, 0);

    // NodeのCryptoAPIで暗号化
    const cipher = createCipheriv(algorithm, key, iv);
    const encrypted = Buffer.from(await cryptByNodeApi(cipher, plainText), "hex").buffer;

    // Nodeのcipherに該当するものを作る
    const keyForbrowser = await importKeyByBrowserApi(key);
    // ブラウザのCryptoAPIで復号化
    await decryptByBrowserApi(encrypted, keyForbrowser, iv);
  }
};

注意点として以下のことがあげられる。

  • 公式ドキュメントとは異なりAESの256bit(32Byte)なのでキーは32Byteになる。
  • IVは16Byte固定。
    • ソースでは0固定にしているが本来は値を与えること。
  • cryptByNodeApi()ではhexにしているため、ブラウザAPIへの入力に合わせるためArrayBufferを取り出している。

ブラウザAPIの方はArrayBufferを与えないとエラーになるが、実際何がArrayBufferでなくてはいけないのかわからなくてハマった:persevere:
SubtleCrypto.decrypt()のドキュメントを見るとBufferSourceとなっており、リンク先に行かないと気が付かない罠。

data is a BufferSource containing the data to be decrypted (also known as ciphertext).

BrowserのCrypto

基本的には公式ドキュメント先のコードがそのまま使用できる。
https://github.com/mdn/dom-examples/blob/master/web-crypto/encrypt-decrypt/aes-cbc.js

大して見どころはないが、私のソースも載せておく。
ライブラリの方のソース。

browserCrypto.js
async function cryptByBrowserApi(plainText, key, iv) {
    console.log('平文: ' + plainText);
    console.log("crypt.key:", key);
    console.log("crypt.iv:", iv);

    const encrypted = await window.crypto.subtle.encrypt(
        {
            name: "AES-CBC",
            iv
        },
        key,
        new TextEncoder().encode(plainText)
    );
    console.log('暗号化:', encrypted);
    console.log('暗号化:', Buffer.from(encrypted).toString('hex'));
    return encrypted;
}

async function decryptByBrowserApi(encrypted, key, iv) {
    console.log("decrypt.encrypted:", encrypted);
    console.log("decrypt.key:", key);
    console.log("decrypt.iv:", iv);
    const decrypted = await window.crypto.subtle.decrypt(
        {
            name: "AES-CBC",
            iv,
        },
        key,
        encrypted
    );

    const plainText = new TextDecoder().decode(decrypted);
    console.log('復号化:', plainText);

    return plainText;
}

async function importKeyByBrowserApi(rawKey) {
    const key = await window.crypto.subtle.importKey(
        "raw",
        rawKey,
        "AES-CBC",
        true,
        ["encrypt", "decrypt"]
    );
    return key;
}

async function generateKeyByBrowserApi() {
    const key = window.crypto.subtle.generateKey(
        {
            name: "AES-CBC",
            length: 256
        },
        true,
        ["encrypt", "decrypt"]
    );
    return key;
}

export {
    cryptByBrowserApi, decryptByBrowserApi, generateKeyByBrowserApi, importKeyByBrowserApi
}

注目ポイントは、importKey()とdecrypt()の二つを使用しないといけないところ。
importKey()であっているのだろうか?rawKeyは正しく指定しているのか?ArrayBufferじゃないといけないのエラーって何?
複数の誤りでエラーポイントが特定できず完成までに1日も消耗してしまった。

rawの中身については公式ドキュメントのソースの1行目に具体的にある。
https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#Raw

const rawKey = window.crypto.getRandomValues(new Uint8Array(16));

しかし、次の行でfunction importSecretKey(rawKey) {ともなっており、rawKeyは引数しかないと思ってしまった。
Uint8Array(16)ってちゃんとあるのに:weary:
ブラウザで暗号化する場合、key指定不要のgenerateKey()を利用するため、Node.jsのkeyを使えるのかもその時はわかっていなかった。

加えて生成されるCryptoKeyの中身が見れないのが、問題解決を遅らせた。
CryptoKeyがおかしいのか、decrypt()がおかしいのか見当がつかなかった。
これを間違わなければ1時間もあれば終わるようなもの。。。

Conclusion

JavaScriptは型を宣言しないとはいえ、builtinAPIはTypeScriptの型が見みれる。(複数の入力があるためどれがどれに対応するかはわからないが)
それにもかかわらず何とかなるだろうと、詳しく見ずにリトライを繰り返したのがよくなかった。

丁寧に見ていけば大丈夫…なはず。Node.jsは怖くない:relaxed:

Have a great day!

Appendices

今回のコードをブラウザで動かせるようにしたソースコード。
自分用なので少し不親切なのに注意。

ブラウザで動作確認(Node.APIはbrowserifyが使用される)

terminal

npm start

純粋なNode.APIでの確認

terminal

node -r esm ./src/cli.js

https://github.com/qrusadorz/example-decrypt-in-browser

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

vue-routerで親子間でのイベント処理

概要

vue-router使用時の「子→親」あるいは「子1→子2」のイベント渡しの記述方法について自分用メモ

:warning:ソースコードは簡略化したもので、ちゃんとテストしていないので動かないかも

サンプルコード

this.$router.app.$on を扱うのがポイント。それ以外は通常のイベントバスのやり方と同じ。

主要な部分

app.js
//(前略)
const app = new Vue({
    router, // これが「ルーターインスタンスを root インスタンスに router オプションとして」かな?
    el: '#app',
    render: h => h(App)
});

親コンポーネント

parent.vue
<template>
    <div>  
        <child1></child1>
        <child2></child2>
    </div>
</template>

<script>
// 子2つを配置するのみ
import component_child1 from './child1.vue'
import component_child2 from './child2.vue'

export default {
    components: {
      'child1': component_child1,
      'child2': component_child2
    }
};
</script>

子コンポーネント(2つ)

目標とする動作は下記。

  1. child1でボタンを押すと test_method_child1 が実行される。
  2. test_method_child1 内で test_event_global がスローされ、child1は非表示。
  3. child2でイベントの解除(重要、後述。参考資料を参照。)
  4. child2ではイベントを test_event_globaltest_event_child2 としておく。
  5. child2でイベントを受け取ったら、v-on:test_event_child2test_method_child2 が実行。
  6. child2が表示され、Hello Worldが見える。
child1.vue
<template>
    <div v-show="visible_child1">
       <button v-on:click="test_method_child1">Run Test</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            visible_child1 : true  // 表示/非表示制御
        };
    },
    methods: {
        test_method_child1() {
            this.visible_child1 = false;
            // イベントバスを this.$router.app として、イベントをスロー
            this.$router.app.$emit('test_event_global');
        }
    }
}
</script>
child2.vue
<template>
    <div v-on:test_event_child2="test_method_child2" v-show="visible_child2">
       Hello world !!
    </div>
</template>

<script>
export default {
    data() {
        return {
            visible_child2 : false // 表示/非表示制御
        };
    },
    mounted() {
        // イベントバスを this.$router.app として、コンポーネントのイベントに紐づけている
        this.$router.app.$off('test_event_global');
        this.$router.app.$on('test_event_global', this.test_event_child2);
    },
    methods: {
        test_method_child2() {
            this.visible_child2 = true;
        }
    }
}
</script>

注意点

解除が行われない、というより .$off をしないと蓄積するようです

ページ遷移してcreatedやmountedが何度も呼ばれると、そのたびに蓄積されてしまうらしい。


上記は例。$onでメソッドを直接叩くこともできるはず。
表示/非表示みたいなことを実現するだけならv-onは取ってしまって

child2.vue
<div v-show="visible_child2">
(中略)
this.$router.app.$on('test_event_global', this.test_method_child2);

とすることも。

参考資料

  1. Vue Router / API リファレンス
  2. Vue.js のグローバルイベントバスでの親子間以外の通信と vue-router
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者によるプログラミング学習ログ 239日目

100日チャレンジの239日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

239日目は

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

ユーザー目線のVueコンポーネント実装

この記事について

Vueを開発・運用してアプリケーションの規模が段々増えてきました。

チームの人員も増やしていく中で慣れていないエンジニアがコンポーネントの実装に関わるというケースが増えてきて、どんなデータ構造やアクション定義を作るのが良いのか悩んでいるケースを見受けるようになりました。

今回は自分がコンポーネントを実装する時に考えていることを簡単に紹介しておきます。

コンポーネントはユーザーに最も近いコード

コンポーネント思考で実装していくと、ユーザーと近い距離に居るコードは各コンポーネント郡になります。言い換えるとコンポーネントとユーザーは密接な関係にあり、お互いの共通認識が合っていることが望ましいということになります。

利用するユーザーの思い浮かべるデータ構造や行動と実装されたコードの変数や関数が同じであるということです。つまりコンポーネントを使うユーザー側の気持ちがわからなければコンポーネントの実装も困難ということです。

サンプルです。

<template>
  <div>
    ...何かしらのコンテンツ

    <button @click="toggleFavorite">
      <template v-if="favorite">
        いいね!を解除
      </template>

      <template v-else>
        いいね!する
      </template>
    </button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data() {
    return {
      favorite: false
    }
  },
  methods: {
    toggleFavorite() {
      this.favorite = !this.favorite
    }
  }
})
</script>

何かしらのコンテンツに対していいね!したりするサンプルです。この実装には2つの問題点があります。

  • ユーザーにとってはいいね!なのにコード上ではお気に入りという変数名になっている
  • いいね!を変更する処理がコード上では同一の関数で定義されている

どちらも動作上は問題ありません。バグもありません。ですが後々の機能追加や変更などで破壊的変更をする必要が出てしまうコードで良いコードとは言えません。何故ならユーザーの行動原理から外れている実装だからです。

ユーザー側の名称でデータ構造を定義

favorite をいいね!として扱う場合、機能追加で「お気に入りを追加したい」と言われた時にどうでしょうか? favorite2 と定義してやり過ごすことはできますが、今後コードを見た人が混乱してしまうと思います。

古くから運用しているサービスになると歴史上データベースやAPIが favorite になっているケースがあります。サンプルではバックエンドと連携していませんが、こういったアプリケーションのサービス内の名称とユーザー側の名称が違う問題はそのままコンポーネントの実装にまで影響してしまうケースが多くあります。

統一することが理想ですがデータベースごと名称の変更を全て反映するのは規模によっては難しいです。でも差異を無くしておくことはできます。

フロントエンドは比較的変更がやりやすい分類なのでこういったものは先にフロントエンド側で吸収しておくことで、機能追加や改善・インフラ整備の時に役立つことになるでしょう。

ユーザーの行動に沿った関数を定義

toggleFavorite という関数は効率的です。ですがユーザーの意思とは連動していません。

極端な例ですが、例えば「誤動作を避ける為にいいねする時だけ確認のアラートをして欲しい」といったユーザーの要望を反映する時にどうでしょうか?両方の機能を内包している為、片方のアクションに対して何か付加価値を付け足す時に拡張しにくいと感じませんか?

ユーザーにとって「いいね!する」という行動と「いいね!を解除する」という行動は全く別の意思から行われるアクションです。こういったユーザーの行動ごとに特殊な処理を挟むケースはフロントエンドではとても多いです。経験上Vuexでステート管理すると更に複雑化していきます。

「関数は最低限の方が...」「HTML上では同じ場所にあるボタンなのに...」と思うこともあるかもしれません。ですが、あくまでアプリケーションは利用するユーザーの為に動くべきであり、機能追加や変更もユーザーの行動結果から発生していくものです。

修正後のコードです。

<template>
  <div>
    ...何かしらのコンテンツ

    <button v-if="like" @click="unlikeContent">
      いいね!を解除
    </button>

    <button v-else @click="likeContent">
      いいね!
    </button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data() {
    return {
      like: false
    }
  },
  methods: {
    likeContent() {
      this.like = true
    },
    unlikeContent() {
      this.like = false
    }
  }
})
</script>

コンポーネントの実装 = ユーザーの意思

この考え方はテストケースも作成しやすい利点があります。

toggleFavorite は現実のユーザーの行動と反しています。 likePostunlikePost を実行してデータ構造をチェックする方がよりユーザーの行動に近いテストケースになると思います。

同じようにバグの発生源を特定する上でも有効です。問い合わせなどのユーザーの行った行動がそのまま再現手順に近くなり、関連するデータやメソッドに対してデバッグを行えば見つかりやすくなります。

実装上ではとても小さなことですが、こういった考えが規模の大きくなったアプリケーションにおいて効果的になっていきます。まずはユーザーの気持ちになって脳内でコンポーネントを操作するイメージを掴むことが大事です。

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

【Nuxt.js】Vuex基礎編:コードをスッキリさせよう

前置き

Happy Valentine❤️?
ということで(?)本日はVuex!
3つに分けて書きます✍️
・vuexとは何か
・メリットは
・簡単な使い方

公開予定日を過ぎ、
申し訳ございません?‍♀️
自社開発アプリを進めておりました?

Vuexとは

状態管理のライブラリです。
って言ってもイメージ沸かないので…

簡単に言うとデータ保存ができるもの
・ログイン情報が
 ページ遷移後も保持されたり、
・APIのデータ引っ張ってきて保存とか、
・methodsに何度も同じ処理書かずに済む
とかとか。
$emitも何回も書かなくていいし楽!
とにかく楽!!!そんな感じです。

状態保持をするのでstoreに書きます✍️

メリット

上記の内容がそのままメリット
コードもまとまって楽ちんなのは
このあとの未使用・使用の比較
を見ていただければ分かります?

使い方

とにかくまずインストール

ターミナル
npm install vuex --save

https://vuex.vuejs.org/ja/installation.html

使い方(超簡単ver.)

vuex.gif

まずは分かりやすく
components使わず
pagesだけで完結させます。

store/index.js
export const state = () => ({
 counter: 0
})

export const mutations = {
 increase(state) {
   state.counter++
 },
 decrease(state) {
   state.counter--
 }
}
index.vue
<template>
 <div class="container">
   <button @click="increase">increase</button>
   <button @click="decrease">decrease</button>
   {{ $store.state.counter }}
 </div>
</template>

<script>
export default {
 methods: {
   increase () {
     this.$store.commit('increase')
   },
   decrease () {
    this.$store.commit('decrease')
   },
 },
}
</script>

??
今度はvuexを使用しない場合
それぞれの対応箇所が分かりますね。
・dataがstate
・methodsがmutations

index.vue
<template>
<div class="container">
  <button @click="increase">increase</button>
  <button @click="decrease">decrease</button>
  {{ counter }}
</div>
</template>

<script>
export default {
 data () {
   return {
     counter: 0,
   }
 },
methods: {
  increase () {
   this.counter++;
  },
  decrease () {
   this.counter--;
  },
},
}
</script>

使い方(Components使用ver.)

vuex.gif

作るものは全く同じです。
コンポーネント化するだけです!

?Point
・index.jsはバグるので新しいjsファイルを作成。
・そのため呼び出し方が変わります。
・gettersでstateの状態を取得する必要あり。

counter.js
export const state = () => ({
 counter: 0
})
export const mutations = {
 increase(state) {
   state.counter++
 },
 decrease(state) {
   state.counter--
 }
}
export const getters = {
 counter: state => {
   return state.counter
 }
}

Counter.vue(子コンポーネント)
ここで呼び出し方が
'ファイル名/定義した物' に変わります?

Counter.vue
<template>
<div class="container">
  <button @click="$store.commit('counter/increase')">increase</button>
  <button @click="$store.commit('counter/decrease')">decrease</button>
  {{ counter }}
</div>
</template>

<script>
export default {
 computed: {
   counter () {
     return this.$store.getters['counter/counter']
   },
 },
}
</script>

index.vue(親ページ)
親には何も書かず超スッキリ??

index.vue
<template>
 <div class="container">
   <Counter />
 </div>
</template>

<script>
import Counter from '~/components/Counter.vue';

export default {
 components: {
   Counter,
 },
}
</script>

??
vuexを使わないと
$emitとか親でどう処理するかとか
いちいち分けたり何度も書く必要が出てきます。

Counter.vue(子コンポーネント)
<template>
<div class="container">
  <button @click="$emit('increase')">increase</button>
  <button @click="$emit('decrease')">decrease</button>
  {{ counter }}
</div>
</template>

<script>
export default {
 props: {
   counter: Number
 },
}
</script>
index.vue(親ページ)
<template>
 <div class="container">
   <Counter
     :counter="counter"
     @increase="increase"
     @decrease="decrease"
   />
 </div>
</template>

<script>
import Counter from '~/components/Counter.vue';

export default {
 components: {
   Counter,
 },
 data () {
   return {
     counter: 0
   }
 },
methods: {
  increase () {
    this.counter++;
  },
  decrease () {
    this.counter--;
  }
},
}
</script>

記事が公開したときにわかる様に、
note・Twitterフォローをお願いします?
https://twitter.com/aLizlab

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

Leafletのマーカー(アイコン)にgeoJSONから読み込んだデータ(番号・数字・文字列)を表示する

まとめ

・stackoverflowのこの記事を参考に、Awesome-Markersプラグインを改修してマーカー(アイコン)にgeoJSONから読み込んだデータを載っけた。
Screen Shot 2020-02-14 at 6.32.18 pm.png

やろうとしたこと

以下のようなGeoJSONファイルを.getJSON()でJavaScriptに取り込み、Leaflet上で"number"をアイコン上に表示させる。

{
    "type": "FeatureCollection",
    "crs": {
        "type": "name",
        "properties": {
            "name": "EPSG:4326"
        }
    },
    "features": [
        {
            "type": "Feature",
            "properties": {
                "title": "皇居",
                "number": 1
            },
            "geometry": {
                "type": "Point",
                "coordinates": [
                    139.75441656790684,
                    35.68826105264495
                ]
            }
        },

    ]
}

実装イメージ(画像引用元
Screen Shot 2020-02-14 at 5.54.55 pm.png

やり方

1. プラグインのインストール

チュートリアルや技術ブログ(例えばkitanoteさんとか)などを参照し、Awesome Markersプラグインをインストールします。
GitHubから必要なファイルをダウンロードし、適したディレクトリに保存します。
.htmlの<header><header/>部分で、先ほど保存した.jsファイル、.cssファイルにパスを通し、合わせて、フォント(Awesome Font)の参照先をURLで指定します。

  <!-- plugin -->
    <!--プラグインの.jsファイル、.cssファイルにパスを通す-->
        <link rel="stylesheet" href="leaflet.awesome-markers.cssの置き場所を指定">
        <script src="leaflet.awesome-markers.jsの置き場所を指定"></script>
    <!-- フォント(Awesome Font)の参照先を指定 -->
        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp" crossorigin="anonymous">
    <!-- plugin -->

2. stackoverflowの記事を参照しつつプラグインを改修する

Leaflet Awesome-Markers (Adding Numbers)の二人目の回答者であるrockXrock氏の2., 3. 及び5.の説明に従い、leaflet.awesome-markers.js を改修します。

  1. change leaflet.awesome-markers.js line 2, add html:""
  2. change leaflet.awesome-markers.js line 80,
  3. comment out line 45 and 47.

3. JSONファイルを取り込んでLeafletで表示させるスクリプトを書く

rockXrock氏の4. の説明を参考に、アイコンの設定をJavaScriptに記載する

4.when creating icon, call like before

$.getJSON("ファイル名もしくはパス", function(data) {
    var geojson = L.geoJson(data,  {
      pointToLayer: function (feature, coordinates) {
           return L.marker(coordinates, {icon:
              L.AwesomeMarkers.icon({icon: '',
              markerColor: 'darkblue',
              prefix: 'fa',
              html: (feature.properties.number)})})},
    });
    geojson.addTo(map);
});

$.getJson("ファイル名もしくはパス", data){} でjsonファイルを読み込み、その後の処理を定義します。

var geojson = L.geoJson(data, {})でgeojsonという名の関数を定義します。
L.geoJson()はリーフレットのライブラリのうちGeoJSONを扱うオブジェクトです。公式ドキュメントを参考に、{}内にオプションを入れていきます。

pointToLayer: function (feature, coordinates) {return L.marker(coordinates, {})で、JSONファイル(ここではfeature)からcoordinatesフィールドにある緯度経度データを読み取り、緯度経度に基づいてマーカーを表示させます。
ここの{}内で、先ほど編集したAwesome Markersプラグインのアイコン設定を参照させます。

最後に、geojson.addTo(map);で、定義したgeojson関数を地図に追加しています。

アイコンの設定の詳細を確認しますと、、、

{icon:L.AwesomeMarkers.icon({
     icon: '',
     markerColor: 'darkblue',
     prefix: 'fa',
     html: (feature.properties.number)})})}

L.AwesomeMarkers.icon() で先ほど改修したleaflet.awesome-markers.js内に定義されているicon関数を呼び出します。
html: (feature.properties.number)でマーカーに表示させるデータのフィールドを指定します。今回は、読み込んだJSONファイル(feature)の中のプロパティのうち、numberフィールドを指定しました。

4. 実装結果

桁数が多いとはみ出したりしますが、登録されたデータから引っ張ってきた情報をアイコンに載せることに成功しました。
Screen Shot 2020-02-14 at 6.34.55 pm.png

失敗した方法

アイコンの設定方法には、var を使って設定を事前に関数として定義し、それを {icon: 名前} で引用するという方法もありますが、以下の通り numberedIcon として外出ししてみましたが、データの継承の問題?でfeatureが定義されてないよ!という以下のエラー文が出てしまいました。
Uncaught ReferenceError: feature is not defined
これを回避するため、今回はL.geoJSON()の中に直接アイコン設定を書き込んだ次第です。

var numberedIcon = L.AwesomeMarkers.icon({
icon: '',
markerColor: 'darkblue',
prefix: 'fa',
html: (feature.properties.number)
});

$.getJSON("ファイル名もしくはパス", function(data) {
    var geojson = L.geoJSON(data,  {
      pointToLayer: function (feature, coordinates) {
           return L.marker(coordinates, {icon:numberedIcon})})},
    });
    geojson.addTo(map);
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js - Firebaseへのデプロイ & 独自ドメインの設定

deploy.png

Nuxt.jsで作成したSPAアプリケーションをFirebaseにデプロイする方法をいつも忘れるので、メモしておきます。

プロジェクトの追加

Firebaseへ行き、Googleアカウントでログイン後、コンソールに移動します。

"プロジェクトを追加"から、プロジェクトの追加を行います。

スクリーンショット 2020-02-14 13.07.05.png

スクリーンショット 2020-02-14 13.08.44.png

・Googleアナリティクスの設定

ここでは、とりあえず無効にしておきます。

有効にすると、アナリティクスアカウントの設定が必要です。

スクリーンショット 2020-02-14 13.10.38.png

これで完了です。

スクリーンショット 2020-02-14 13.12.45.png

firebase-tools

次に、firebase-toolsのインストールを行います。

$ npm install -g firebase-tools

インストール後、ログインします。

ブラウザが開くので、GoogleアカウントでログインすればOKです ;)

$ firebase login

デプロイしたいプロジェクトのディレクトリに移動し、init

$ firebase init

いくつかの質問があります。

今回はHostingを利用するので、スペースで選択し、Enter。

スクリーンショット 2020-02-14 13.25.10.png

次に、"Use an existing project" を選択します。

冒頭の「プロジェクトを追加」で追加した、プロジェクト名を選択します。

スクリーンショット 2020-02-14 13.29.14.png

"dist"と入力。

スクリーンショット 2020-02-14 13.32.53.png

SPAとして設定するかを聞かれます。

私は、SPAで作成しているので、yを入力します。

スクリーンショット 2020-02-14 13.34.39.png

これで設定は完了です!

Deploy

$ npm run generate
$ firebase deploy

完了すると、最後にURLが表示されるので、アクセスしてみます。

無事、Firebaseにデプロイすることができました :)

スクリーンショット 2020-02-14 13.39.50.png

コードを追加したり、修正した場合には、再び以下のコマンドを打てばOKです ;)

$ npm run generate
$ firebase deploy

スクリーンショット 2020-02-14 15.14.05.png

独自ドメインの設定

Firebaseのコンソールページに行き、独自ドメインを設定したいプロジェクトを選択します。

次に、左サイドバーのHostingを選択。

スクリーンショット 2020-02-14 13.51.51.png

"カスタムドメインを追加"をクリック。

スクリーンショット 2020-02-14 13.55.36.png

あとは、お名前.comなどで取得したドメインを入力し、従えばOKです ;)

反映するのに、少し時間がかかると思います。

スクリーンショット 2020-02-14 13.58.05.png

説明不足だと感じた点や、変更点が出てきましたら、随時更新していきたいと思います。

この記事が参考になりましたら幸いです。

Thanks,
@ShogoMurakami

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

リンク画像にマウスホバーすると画像がふわっと(重要)変化する方法

これからすること

ユーザビリティの観点から、リンクできる画像やボタンなどのパーツには ”リンクできそう” な見た目や演出が要求されるのですが顧客受けが良いのか
画像にマウスのカーソルを乗せるとふわっと変化する
を求められる場面が結構あります。
ホバーした時に画像を変えるだけなら

$(function(){
  $('a img').hover(function(){
    $(this).attr('src',$(this).attr('src').replace('_off','_on'));
  },function(){
    $(this).attr('src',$(this).attr('src').replace('_on','_off'));
  });
});

img01_on と img01_off という2種類の画像を用意して JSに ↑ を書くだけで対応できるのですがふわっとすることなく一瞬で変わってしまします。

解決方法(JS不要)

See the Pen poJgWeQ by sphenisc (@sphenisc) on CodePen.

CSSでホバーしていない状態の画像を上から被せておき、ホバーするとimgタグで入れた方の画像が現れる仕組みになっています。

  transition: all 1s;

の部分をいじれば変化のさせ方を変えることができます。

ul li a:hover::after {
  opacity: 0;
}

の部分に height:0; を追加するだけでカーテンが上がるような動きにできます。

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

JSで要素間を線で繋いだり動かしたりライブラリ調査@202002

JSでオブジェクト間に線を引きたい調査メモ

ライブラリ名 GitHub サンプル
diagramflowjs https://github.com/luisalvesmartins/diagramflowjs https://lambot.blob.core.windows.net/github/diagramflowjs/index.html
SimpleFlowchart https://github.com/mmazo/SimpleFlowchart http://mmazo.de/flowchart/
svg.connectable.js https://github.com/jillix/svg.connectable.js http://jillix.github.io/svg.connectable.js/
svg.draggy.js https://github.com/jillix/svg.draggy.js http://jillix.github.io/svg.draggy.js/
leader-line https://github.com/anseki/leader-line https://anseki.github.io/leader-line/

diagramflowjs

削除や追加、リネームなど機能が豊富な感じで実用向けな感じ

https://lambot.blob.core.windows.net/github/diagramflowjs/index.html

NoName_2020-2-14_11-52-43_No-00.png

SimpleFlowchart

バインバイン動く

http://mmazo.de/flowchart/

NoName_2020-2-14_11-53-42_No-00.png

svg.connectable.js

ソースコードなどが合って使いやすい

http://jillix.github.io/svg.connectable.js/

NoName_2020-2-14_11-55-2_No-00.png

svg.draggy.js

SVGドラック簡単に追加できて便利

http://jillix.github.io/svg.draggy.js/

NoName_2020-2-14_11-54-54_No-00.png

leader-line

結構いろんな線があるのでウェブで線を引きたいときには使いたい感じ

https://anseki.github.io/leader-line/

NoName_2020-2-14_11-55-42_No-00.png

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

【JavaScript】Vue.js(Element UI)を使用している際に「async-validator」単体で使う場合のカスタムバリデーション実装方法

概要

Vue.js(Element (Element UI))を使用している場合に「async-validator」単体で使う実装方法例を少々

インストール(npm)

async-validator - npm

npm i async-validator

※筆者はElement (Element UI)(Vue.jsのコンポーネントライブラリ)を使用しているので別途インストールしなくても使える状況ですので単体でインストールしたことはないです

実装例

簡単なアカウント登録画面の例として以下のように実装してみました

form.vue
<template>
    <div class="form">
        <div class="text-center">アカウント登録</div>

        <input type="email" v-model="ruleForm.email" placeholder="メールアドレス*">
        <input type="password" v-model="ruleForm.password" placeholder="パスワード*">
        <input type="password" v-model="ruleForm.passwordConf" placeholder="パスワード(確認)*">

        <div class="text-center">
            <input type="button" @click="beforeSubmit" value="登録">
        </div>

    </div>
</template>

<script>
import schema from 'async-validator';

export default {
    data() {
        // カスタムバリデーション定義する場合はここに記述
        var validatePasswordConf = (rule, value, callback) => {
            if (value === '') {
                callback(new Error('パスワード(確認)を入力してください。'));
            } else if (value !== this.ruleForm.password) {
                callback(new Error('二つのパスワードが異なります。'));
            } else {
                callback();
            }
        };
        return {
            // フォーム用変数
            ruleForm: {
                email: '',
                password: '',
                passwordConf: '',
            },
            // バリデーションルール定義(項目毎)
            rules: {
                email: [
                    { required: true, message: 'メールアドレスを入力してください。', trigger: 'blur' },
                    { type: "email", message: '正しいメールアドレスを入力してください。', trigger: 'blur' },
                    { min: 1, max: 255, message: '1文字以上255文字以内で入力してください。', trigger: 'blur' }
                ],
                password: [
                    { required: true, message: 'パスワードを入力してください。', trigger: 'blur' },
                    { min: 1, max: 50, message: '1文字以上50文字以内で入力してください。', trigger: 'blur' }
                ],
                passwordConf: [
                    // 上記で定義したカスタムバリデーションを使用
                    { validator: validatePasswordConf, trigger: 'blur' }
                ],
            },
        }
    },
    methods: {
        beforeSubmit() {
            var validator = new schema(this.rules);
            validator.validate(this.ruleForm, (errors, fields) => {
                if(errors) {
                    // バリデーションエラー時
                    return alert(errors[0]["message"]);
                }
                // バリデーションパス
                return alert("登録しました");
            });
        },
    }
}
</script>

バリデーションのルール詳細は「async-validator」のREADMEとかを参考にしてください。

参考URL

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

[Rails] scriptタグ内でRuby/Railsのメソッドを使用する際のescape_javascriptについて

この記事を書くきっかけ

jQueryを書いていると、scriptタグ内でRailsのメソッド(正確にはapplication_helper内のメソッド)を使用したいときがあります。
正しい書き方をしないと正確に呼び出せないため、失敗した書き方と成功した書き方をまとめます。

前提

current_userの属性によってpathを切り替えるメソッドを
def hogehoge_path_forとし、application_helper.rbに定義します。

application_helper.rb
def hogehoge_path_for
  if ...
   hoge_path
  else
   hogehoge_path
  end
end

これをとあるviewのscriptタグ内で呼び出します。
let path = hogehoge_path_forの返り値
としたいです。

失敗例1

あるview
<script>
  let path = <% hogehoge_path_for %>;
</script>

普通はこう考えるはずですが、これだと呼び出せません。

失敗例2

あるview
<script>
  let path = <% hogehoge_path_for %>;
</script>

これも呼び出せません。

失敗例3

escape_javascriptというメソッドを使用します。
なお、これはaliasとしてjの使用が許されています。

あるview
<script>
  let path = <% escape_javascript hogehoge_path_for %>;
</script>

上記は下記と同じです。

あるview
<script>
  let path = <% j hogehoge_path_for %>;
</script>

どちらも失敗します。

失敗例4

=をつけます。

あるview
<script>
  let path = <%= escape_javascript hogehoge_path_for %>;
</script>

上記は下記と同じです。

あるview
<script>
  let path = <%= j hogehoge_path_for %>;
</script>

これもどちらも失敗します。

失敗例5

=を外して、''で囲います。

あるview
<script>
  let path = '<% escape_javascript hogehoge_path_for %>';
</script>

上記は下記と同じです。

あるview
<script>
  let path = '<% j hogehoge_path_for %>';
</script>

これもどちらも失敗します。

成功例

=をつけて''で囲います!

あるview
<script>
  let path = '<%= escape_javascript hogehoge_path_for %>';
</script>

上記は下記と同じです。

あるview
<script>
  let path = '<%= j hogehoge_path_for %>';
</script>


これで無事呼び出すことができました。

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

TensorFlow.jsを使ってjavascript上でkerasモデルを使う

javascriptでTensorFlowモデルを使えるようにするTensorFlow.jsの基本的な使い方をまとめます

公式チュートリアル
https://www.tensorflow.org/js/tutorials

tensorflowjs APIドキュメント
https://js.tensorflow.org/api/latest/

実装例
https://github.com/tensorflow/tfjs-examples

試行環境

Windows10

Node.js版とブラウザ版

tensorflowjsはC++で書かれたtensorflowのwrapperですが、クライアントサイドのブラウザで動作するjavascript版とサーバーサイドで動作するNode.js版があります。Node.js版はローカル環境に最適化された追加ライブラリで高速に動作しますしCUDAを使ってGPUもフルに使えるようになっているんだそうです。ブラウザ版はCPUのみで比較してもNode.jsと比べると速度は劣りますがクライアントのリソースを使えるのが大きなメリットです。

pandasが使えるpython版と比べるとNode.js版でも使い勝手は悪いので、pythonでモデルを作ってコンバートしたものをNode.jsをバックエンドとする環境でNode.js上で使うか、サーバーサイドの負荷を低減するためにブラウザ上のjavascriptで使うというやり方が多くなるのではないかと思います。

python版tensorflowで作ったmodelを変換する

1. python版のモデルを用意する

model.save()で保存したh5ファイルを変換することが出来ます。
とりあえず入力1次元、出力1次元の回帰モデルを作って保存してみます。

python
import numpy as np
import pandas as pd
import tensorflow as tf

physical_devices = tf.config.list_physical_devices('GPU')
if len(physical_devices) > 0:
    tf.config.experimental.set_memory_growth(physical_devices[0], True)
    print('memory growth:', tf.config.experimental.get_memory_growth(physical_devices[0]))

x = np.array([1,2,3,4]).reshape(4, 1)
y = np.array([1,3,5,7]).reshape(4, 1)

inputs = tf.keras.layers.Input(shape=(1))
outputs = tf.keras.layers.Dense(1, activation='linear')(inputs)
model = tf.keras.models.Model(inputs=inputs, outputs=outputs)
opt = tf.optimizers.Adam(lr=0.1)
model.compile(optimizer=opt, loss='mse')

model.fit(x, y, epochs=100, batch_size=1, verbose=0)
print(model.predict(x))

model.save('../model.h5')
pd.DataFrame(x).to_csv('../x.csv')

2. モデルを変換するtensorflowjsパッケージをインストールする

pythonパッケージのtensorflowjsをpip installして使用します。ただし2020年2月現在、tensorflow 2.0.1~2.1.0とは依存パッケージが競合してしまいますので、tensorflowjs用のconda環境を作って専用にした方が良いと思います。なお、tensorflow 2.1.0で作ったモデルも現状は問題なく変換できるようです。

terminal
pip install tensorflowjs

3. モデルを変換する

tensorflowjsをインストールしたpythonにPATHが通った環境で、tensorflowjs_converterを使って変換します。以下の場合、カレントディレクトリのhoge.h5ファイルを読み込んで、/model/以下にモデル構造を記述したjsonファイルと重みを記録したbinファイルを書き出してくれます。

terminal
tensorflowjs_converter --input_format keras hoge.h5 model

ブラウザ版tensorflowjsで使う

tf.loadLayersModel()でjsonファイルのPATHを指定してやると保存したモデルを読んで、tf.tensorを放り込むと推定してくれます。ただし、ブラウザ上で動作するjavascriptはローカルのファイルを読めなくなっているので、面倒ですが適当なウェブサーバーを立ててドキュメントルート以下に上記で変換したモデルをフォルダごと置いてブラウザでURLを読み込むようにします。

フォルダ構造
└─document root
    │  index.html
    │  
    ├─model
       model.json
       group1-shard1of1.bin
index.html
<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
    <script>
      async function run(){
        // load model
        const path = "http://localhost/model/model.json";
        const model = await tf.loadLayersModel(path);

        // predict
        const xs = tf.tensor2d([1, 2, 3, 4], [4, 1]);
        y_pred = await model.predict(xs);
        y_pred.print();

        // convert to array
        const values = await y_pred.data();
        const arr = await Array.from(values);
        console.log(arr);
      }
      run();
    </script>
  </body>
</html>

Node.js版で使う

1. Node.jsをインストールする

Node.jsがまだ入ってないようならインストールします
https://nodejs.org/ja/

インストールするとPATHが通ってコマンドプロンプトから呼べるようになります

コマンドプロンプト
> node -v
v12.14.1

> npm -v
6.13.4

2. Node.jsにプロジェクトを作る

NodeJS環境を初期化します。適当なフォルダを作ってフォルダ内でnpm initすると必要なファイルが作成されます。package nameなどを聞かれますがすべてdefaultで大丈夫です。最後にIs this OK?にyesと回答すれば実行されます。

コマンドプロンプト
mkdir tensorflow-demo
cd tensorflow-demo/
npm init

3. Node.jsプロジェクトにtensorflowjsをインストールする

プロジェクトのディレクトリで以下を実行すればインストールされます。tfjs-nodeはNode.jsに最適化したライブラリで、ブラウザ版で実行するより高速に動作します。

コマンドプロンプト
npm install @tensorflow/tfjs-node

GPU+CUDAがある環境ではnpm install @tensorflow/tfjs-node-gpuすればGPUを使った高速演算ができると書かれているんですが、CUDA10.0をインストールしたWin10でやってみたらMODULE_NOT_FOUNDで動きませんでした。多分PATHが通ってないとかだと思いますが未確認です。

4. Node.jsでpython版modelを使う

tf.loadLayersModel()でjsonファイルのPATHを指定してやると保存したモデルを読んで、tf.tensorを放り込むと推定してくれます。ブラウザ上で動作する場合とは異なりウェブサーバーを立てる必要はありません。

フォルダ構造
└─project folder
    │  index.js
    │  
    ├─model
       model.json
       group1-shard1of1.bin
index.js
// import
const tf = require('@tensorflow/tfjs');
require('@tensorflow/tfjs-node');

async function run(){
 // load model
 const path = "http://localhost/tensorflowjs/tfjs_001/model/model.json"
 const model = await tf.loadLayersModel(path);

 // predict
 y_pred = await model.predict(tf.tensor2d([1, 2, 3, 4], [4, 1]));
 y_pred.print();

 // convert to array
 const values = await y_pred.data();
 const arr = await Array.from(values);
 console.log(arr);
}

run();

index.jsと同じディレクトリからindex.jsを実行するとデモコードが動きます。

コマンドプロンプト
node index.js
実行結果
Tensor
    [[0.9999999],
     [3        ],
     [5.0000005],
     [7.0000005]]
[ 0.9999998807907104, 3, 5.000000476837158, 7.000000476837158 ]

推定結果が表示されれば正常です。

tensorの計算

python版のtensorflowは適当に配列を放り込めばlistだろうがnp.arrayだろうが適当に処理してくれましたが、tensorflowjsではtf.tensor型に変換してからでないと扱えません。tf.tensorにするとjavascript標準の四則演算は出来なくなりますが、tf.add()やtf.sub()などで高速な演算が出来るようになっていますので不足はないと思われます。

四則演算以外にも三角関数とか最大最小とか色々計算できます。
詳しくは以下のAPIドキュメントを参照してください。
https://js.tensorflow.org/api/latest/

tensorとscalarの四則演算
const x = tf.tensor1d([1,2,3])
const a = tf.scalar(4)

x.add(a).print()
x.sub(a).print()
x.mul(a).print()
x.div(a).print()

tensor同士の計算も同じようにできます。

tensor同士の四則演算
const x1 = tf.tensor1d([1,2,3])
const x2 = tf.tensor1d([2,3,4])

console.log('add')
x1.add(x2).print();

console.log('sub')
x1.sub(x2).print();

console.log('mul')
x1.mul(x2).print();

console.log('div')
x1.div(x2).print();

tf.tensorを配列に戻すにはdataSync()メソッドを使います。

配列に戻す
const b = tf.tensor1d([1,2,3])
console.log('b')
b.print()

const b_value = b.dataSync();
console.log('b_value')
console.log(b_value)

メモリの解放

tf.dispose()

openGLでtensorflowjsを使う場合、明示的に変数を消去しないとメモリが解放されない為、明示的にメモリを解放するためにtf.dispose()が用意されています。tf.memory()でメモリが正常にリリースされたか確認できるようになっていますので、tf.dispose()した場合としない場合を比較してみましょう。

tf.dispose()を使わない場合
for (let i = 0; i < 100; i++) {
  const a = tf.tensor1d([1,2,3]);
}
console.log(tf.memory());
tf.dispose()を使った場合
for (let i = 0; i < 100; i++) {
  const a = tf.tensor1d([1,2,3]);
  a.dispose()
}
console.log(tf.memory());

上記をそれぞれ実行するとtf.dispose()を使った場合にはtensorflowjsが保持するメモリがゼロになっている事が確認できます。

tf.tidy()

複数の演算を続けて行った場合に計算途中で生じる変数も残ってしまう為、tf.dity()でこれが残らないように処理するようです。

tf.tidy()の使い方
// y = 2 ^ 2 + 1
const y = tf.tidy(() => {
  // a, b, and one will be cleaned up when the tidy ends.
  const one = tf.scalar(1);
  const a = tf.scalar(2);
  const b = a.square();

  console.log('numTensors (in tidy): ' + tf.memory().numTensors);

  // The value returned inside the tidy function will return
  // through the tidy, in this case to the variable y.
  return b.add(one);
});

console.log('numTensors (outside tidy): ' + tf.memory().numTensors);
y.print();

CSVファイルの読み込み

CSVファイルはtf.data.csv(source)で読み込めます。sourceにPATHを指定して読み込みますが、ブラウザ上で動作するjavascriptはhttpで指定されたPATHしか読めないようになっていますので、ローカルでテストする際にもhttpサーバーに置いて実行する必要があります。Node.jsの場合はそういう制限はないので普通にローカルで実行できます。

take()メソットでデータの頭出し、toArray()メソッドで配列への変換ができます。

test.js
const df = tf.data.csv("http://localhost/hoge.csv")
console.log(df.take(10).toArray())

グラフを描く

javascriptにはpython勢の大好きなmatplotlibがないんですが、ありがたいことに可視化の為のメソッドを用意してくれています。tfvisをインポートすれば簡単にグラフを描画できます。

tfvis APIドキュメント
https://js.tensorflow.org/api_vis/latest/

Node.jsサーバーを立てる

ブラウザ版のテストをする際、Node.jsを使えば簡単にhttpサーバーを立てることができます

コマンドプロンプト
npm install http-server -g
document_rootにするフォルダで以下を実行
http-server -g
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript] スプレッド構文

See the Pen リストにリストの追加 by mykysyk (@mykysyk) on CodePen.

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

お絵かきできるSNSを作りたい!4

今回は少しでもアプリに近づけるためにmanifest.jsonを追加したいと思います。

これを追加することでAndroidでページを開いた際、ホーム画面にタイルを置きませんか?とメッセージを表示することが出来るんだとか。

場所はHEADの中に一行以下の記述を書くだけです。

<link rel="manifest" href="manifest.json">

manifest.jsonの中身は↓こんな感じです。

{
  "lang": "ja",
  "name": "sakura",
  "short_name": "お絵かき",
  "icons": [{
        "src": "icon.png",
        "sizes": "129x129",
        "type": "image/png"
      }],
  "start_url": "https://127.0.0.1/",
  "display": "standalone"
}

言語、アプリ名・短縮アプリ名とタイルに置く画像を指定します。
start_urlはサーバがないので適当です。

最後に自分がやりたそうにしていることをまとめておきます。
・PWA
・線の太さを変える
・線の色を変える
・画像を保存
・レイヤー
・戻る(Ctrl+Z)
・進む(Ctrl+Y)
・Webプッシュ通知対応
・好きな画像を下絵にしてトレースできるようにする

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