20210121のJavaScriptに関する記事は18件です。

蟻コロニー最適化(ACO)で解く巡回セールスマン問題(TSP)をJavaScriptで書いてみた

※個人の練習用です。アルゴリズムの正確性は担保しません。

蟻コロニー最適化(ACO)とは

群知能の一つで、代表的なアルゴリズムです。
他に粒子群最適化、人工蜂コロニーアルゴリズムなどがあります。

蟻コロニー最適化(ACO)は難しい組み合わせ最適化問題の近似解を探索するのに使われるメタヒューリスティックな最適化アルゴリズムである。ACOでは、現実の蟻を真似た人工蟻が問題のグラフ上を移動することで解を構築しようとする。このとき、グラフ上に人工のフェロモンを置くことでその後の人工蟻がよりよい解を探索できるようにする[4]。ACOは多数の最適化問題で効力を発揮してきた。
- Wikipedia: 蟻コロニー最適化

詳細は下のリンクからどうぞ。
アントコロニー最適化(ACO)を救いたい
ACO: アントコロニー最適化

巡回セールスマン問題(TSP)とは

代表的な組合せ最適化問題です。

巡回セールスマン問題(じゅんかいセールスマンもんだい、英: traveling salesman problem、TSP)は、都市の集合と各2都市間の移動コスト(たとえば距離)が与えられたとき、全ての都市をちょうど一度ずつ巡り出発地に戻る巡回路のうちで総移動コストが最小のものを求める(セールスマンが所定の複数の都市を1回だけ巡回する場合の最短経路を求める)組合せ最適化問題である。
- Wikipedia: 巡回セールスマン問題

詳細は下のリンクからどうぞ。
Numerical Optimizer SIMPLE例題集 V19, 2.12 巡回セールスマン問題
巡回セールスマン問題から始まる数理最適化
数理計画用語集 > 巡回セールスマン問題

JavaScriptで作る

  • JavaScriptで作る理由は特にない、ただ気軽に書きたかっただけ
  • アルゴリズムの練習なので、Pure Javascriptを利用
  • コードをもっと綺麗にできるが、練習用なのでとりあえずこのまま
  • ファイルを一つにしているので、そのままコピペしてChromeでも実行できる(おすすめしない)
  • コメント書いてないところはまた時間があれば追記
  • ※ソースコードはGithubにも上げました。

サンプルデータ

サンプルデータはTSPLIBというドイツの大学教授がまとめたTSP問題のデータ集を利用しています。
TSPLIB: http://elib.zib.de/pub/mp-testdata/tsp/tsplib/tsplib.html
TSPLIBデータ集: http://elib.zib.de/pub/mp-testdata/tsp/tsplib/tsp/

今回はTSPLIBのa280を利用しました。最適解も用意されているので、検証に使えます。
データ:http://elib.zib.de/pub/mp-testdata/tsp/tsplib/tsp/a280.tsp
最適解:http://elib.zib.de/pub/mp-testdata/tsp/tsplib/tsp/a280.opt.tour

グローバル設定

// 都市の座標を設定(a280.tspを利用)
const CITIES = [[288,149],[288,129],[270,133],[256,141],[256,157],[246,157],[236,169],[228,169],[228,161],[220,169],[212,169],[204,169],[196,169],[188,169],[196,161],[188,145],[172,145],[164,145],[156,145],[148,145],[140,145],[148,169],[164,169],[172,169],[156,169],[140,169],[132,169],[124,169],[116,161],[104,153],[104,161],[104,169],[90,165],[80,157],[64,157],[64,165],[56,169],[56,161],[56,153],[56,145],[56,137],[56,129],[56,121],[40,121],[40,129],[40,137],[40,145],[40,153],[40,161],[40,169],[32,169],[32,161],[32,153],[32,145],[32,137],[32,129],[32,121],[32,113],[40,113],[56,113],[56,105],[48,99],[40,99],[32,97],[32,89],[24,89],[16,97],[16,109],[8,109],[8,97],[8,89],[8,81],[8,73],[8,65],[8,57],[16,57],[8,49],[8,41],[24,45],[32,41],[32,49],[32,57],[32,65],[32,73],[32,81],[40,83],[40,73],[40,63],[40,51],[44,43],[44,35],[44,27],[32,25],[24,25],[16,25],[16,17],[24,17],[32,17],[44,11],[56,9],[56,17],[56,25],[56,33],[56,41],[64,41],[72,41],[72,49],[56,49],[48,51],[56,57],[56,65],[48,63],[48,73],[56,73],[56,81],[48,83],[56,89],[56,97],[104,97],[104,105],[104,113],[104,121],[104,129],[104,137],[104,145],[116,145],[124,145],[132,145],[132,137],[140,137],[148,137],[156,137],[164,137],[172,125],[172,117],[172,109],[172,101],[172,93],[172,85],[180,85],[180,77],[180,69],[180,61],[180,53],[172,53],[172,61],[172,69],[172,77],[164,81],[148,85],[124,85],[124,93],[124,109],[124,125],[124,117],[124,101],[104,89],[104,81],[104,73],[104,65],[104,49],[104,41],[104,33],[104,25],[104,17],[92,9],[80,9],[72,9],[64,21],[72,25],[80,25],[80,25],[80,41],[88,49],[104,57],[124,69],[124,77],[132,81],[140,65],[132,61],[124,61],[124,53],[124,45],[124,37],[124,29],[132,21],[124,21],[120,9],[128,9],[136,9],[148,9],[162,9],[156,25],[172,21],[180,21],[180,29],[172,29],[172,37],[172,45],[180,45],[180,37],[188,41],[196,49],[204,57],[212,65],[220,73],[228,69],[228,77],[236,77],[236,69],[236,61],[228,61],[228,53],[236,53],[236,45],[228,45],[228,37],[236,37],[236,29],[228,29],[228,21],[236,21],[252,21],[260,29],[260,37],[260,45],[260,53],[260,61],[260,69],[260,77],[276,77],[276,69],[276,61],[276,53],[284,53],[284,61],[284,69],[284,77],[284,85],[284,93],[284,101],[288,109],[280,109],[276,101],[276,93],[276,85],[268,97],[260,109],[252,101],[260,93],[260,85],[236,85],[228,85],[228,93],[236,93],[236,101],[228,101],[228,109],[228,117],[228,125],[220,125],[212,117],[204,109],[196,101],[188,93],[180,93],[180,101],[180,109],[180,117],[180,125],[196,145],[204,145],[212,145],[220,145],[228,145],[236,145],[246,141],[252,125],[260,129],[280,133]];

// 都市間の距離を計算
const DISTANCE_CITIES = calcCitiesDistance(CITIES); 

// スタート地点
const START_CITY = 0;

// 蟻の数
const NUMBER_ANT = 100;
// 蟻の代の数
const LOOP_MAX_COUNT = 100;

// フェロモン計算に使う距離とフェロモンの重みの設定
const WEIGHT_DISTANCE = 3;
const WEIGHT_PHEROMONE = 2;

// フェロモンの量と1代ごとの蒸発度合い
const QUANT_PHEROMONE = 1;
const QUANT_EVAPORATION = 0.8;

ACOコア関数

function runACO(cities){
    // 代ごとのルート情報を一時保存する配列
    let routes =[];
    // 空のルート配列を作る
    let routeCount =  new Array(CITIES.length);
    routeCount = initArray(routeCount, CITIES.length);

    // 全ての代に使うフェロモンの初期設定
    let colonyPheromone = new Array(CITIES.length);
    colonyPheromone = initArray(colonyPheromone, CITIES.length);
    let tmpArr = new Array(CITIES.length);
    tmpArr = initArray(tmpArr, CITIES.length);
    colonyPheromone = updatePheromone(colonyPheromone, tmpArr);

    // 全代分の結果
    let result = [];

    // 設定した代の数だけループ
    for(let loopCount = 0; loopCount < LOOP_MAX_COUNT; loopCount++){(function(){
        console.log('loopCount', loopCount); //今は何代目?

        // 蟻コロニーを作る
        let ants = [];

        // この代のフェロモンの初期設定
        let newPheromone = new Array(CITIES.length);
        newPheromone = initArray(newPheromone, CITIES.length);

        // 設定した蟻の数だけループ
        for(let i=0; i<NUMBER_ANT; i++){(function(){
            // 新しい蟻を一匹作る
            let ant = new InitAnt(START_CITY, colonyPheromone);
            // ルート探索をさせる
            ant.searchRoute(cities);
            // この代のフェロモンを更新
            newPheromone = ant.updatePheromone(newPheromone, cities);
            // ルート計算と保存
            routeCount = ant.countRoute(routeCount);
            if(routes.includes(ant.route) == false){
                routes.push(ant.route);
            }
        }())}

        //全体のフェロモンの更新
        colonyPheromone = updatePheromone(colonyPheromone, newPheromone);
//         console.log('colonyPheromone:', colonyPheromone);
//         console.log('this colony\'s routes:', routes);

        //この代の全てのルートの総距離を計算して結果配列に保存
        result.push(calcRoutesDistance(routes));

        //この代の全てのルート情報をリセット
        routes = [];

    }());
    }

    // 全ての結果を出力
    console.log('Log:', result);

    // 最適解を返す
    return getTheBest(result[result.length-1]);
}

蟻の生成関数

// 蟻のインスタンスを生成するコンストラクタ
function InitAnt(cityNumber, pheromone){
    this.route = [cityNumber];
    this.colonyPheromone = pheromone;
    this.selfPheromone = Array.from(pheromone);
    this.updatePheromone = function(newPheromone, cities){
            for(let i=0;i<this.route.length -1 ;i++){(function(route){
                let thisCity = route[i];
                let nextCity = route[i+1];
                let length = DISTANCE_CITIES[thisCity][nextCity];
                let delta = QUANT_PHEROMONE / length;
                if(isNaN(newPheromone[thisCity][nextCity])){
                    newPheromone[thisCity][nextCity] = delta;
                }else{
                    newPheromone[thisCity][nextCity] += delta;
                }
            }(this.route));
            }
            return newPheromone;        
    };
    this.searchRoute = function(cities){
        let route = this.route;
        let colonyPheromone = this.colonyPheromone;
        for(let i=0;i<cities.length;i++){(function(){
            if(i == cities.length-1){
                route.push(route[0]);
            }else{
                let nextCitiesProbabilities = calcCitiesProbability(route, cities, DISTANCE_CITIES, colonyPheromone);
                let nextCity = Number(selectNextCity(nextCitiesProbabilities));
                route.push(nextCity);
            }
            //console.log(i, route);
        }())
        }
        this.route = route;
//         console.log('this ant\'s route: ', this.route);
    }
    this.countRoute = function(routeCount){
        for(let i=0;i<this.route.length -1 ;i++){(function(){
            let thisCity = i;
            let nextCity = i+1;
            if(routeCount[thisCity][nextCity] == undefined){
                routeCount[thisCity][nextCity] = 1;
            }else{
                routeCount[thisCity][nextCity] += 1;
            }
        }())
        }
        return routeCount;
    }
}

計算用関数群

// 次に行く都市を選ぶ
function selectNextCity(probabilities){
    let cities = Object.keys(probabilities);
    let cumulative = cumulativeSum(Object.values(probabilities));
    let total = cumulative.reduce(function (accumulator, currentValue){return accumulator + currentValue;});
    let random = Math.random() * cumulative[cumulative.length-1];
    let result;
    cumulative.some(function(cumul, index){
        if(cumul > random){
            result = cities[index];
            return true;
        }
    })
    if(isNaN(result)){
        debugger;
    }
    return result;
}

function cumulativeSum(values){
    let result = [];
    for(let i=0; i<values.length; i++){(function(){
        if(i == 0){
            result.push(values[i]);
        }else{
            result.push(result[i-1]+values[i]);
        }
        }())
    }
    return result;
}
// 各都市が選ばれる確率の計算
function calcCitiesProbability(selfRoute, cities, citiesDistance, colonyPheromone){
    let thisCity = selfRoute[selfRoute.length -1];
    if(isNaN(thisCity)){
        console.log('debugger', selfRoute, selfRoute.length, cities, cities.length);
        debugger;
    }
    let numerators = {};
    let probabilities = {};
    let denominator = 0;
    for(let i = 0; i<cities.length; i++){(function(colonyPheromone, citiesDistance, selfRoute){
        if(selfRoute.includes(i) == false){
            //console.log(colonyPheromone.length, citiesDistance.length)
            let thisPheromone = colonyPheromone[thisCity][i];
            let thisDistance = citiesDistance[thisCity][i];
            let numerator = Math.pow(thisPheromone, WEIGHT_PHEROMONE) * (1 / thisDistance);
            numerators[String(i)] = numerator;
            denominator += numerator;
        }
    }(colonyPheromone, citiesDistance, selfRoute));
    }
    Object.keys(numerators).forEach(function(key){
        if(numerators[key] == 0 || denominator == 0){
            probabilities[key] = 0.001;
        }else{
            probabilities[key] = numerators[key] / denominator;
        }
        if(isNaN(probabilities[key])){
            probabilities[key] = 0.001;
        }
    });
    return probabilities;
}
// フェロモンの更新
function updatePheromone(colonyPheromone, newPheromone){
    for(let i=0; i<Math.max(colonyPheromone.length, newPheromone.length); i++){(function(){
        for(let j=0; j<Math.max(colonyPheromone.length, newPheromone.length); j++){(function(){
                    colonyPheromone[i][j] = QUANT_EVAPORATION * colonyPheromone[i][j] + newPheromone[i][j]
        }())
        }
    }())
    }
    return colonyPheromone;
}
// ルート距離の計算
function calcRoutesDistance(routes){
    let result = [];
    for(let i=0;i<routes.length;i++){(function(){
        result[i] = {routes:[],distance:0}
        let distance = calcRouteDistance(routes[i]);
        result[i].routes = routes[i];
        result[i].distance = distance;
//         console.log('Route: ', routes[i],', Distance: ', distance);
    }())
    }
    return result;
}

function calcRouteDistance(route){
    let result = 0;
    for(let i=0;i<route.length-1;i++){(function(){
        let thisCity = route[i];
        let nextCity = route[i+1];
        result+= DISTANCE_CITIES[thisCity][nextCity];
    }())
    }
    return result;
}
// 都市間距離の計算
function calcCitiesDistance(cities){
    let citiesDistance = new Array(cities.length);
    for(let i=0; i<cities.length; i++){(function(){
        citiesDistance[i] = new Array(cities.length);
        for(let j=0;j<cities.length;j++){(function(){
            citiesDistance[i][j] = eucDistance(cities[i],cities[j]);
            }())
        }
        }())
    }
    return citiesDistance;
}

// 都市間のユークリッド距離
function eucDistance(cityA, cityB){
    return Math.sqrt(Math.pow(cityA[0] - cityB[0], 2) + Math.pow(cityA[1] - cityB[1], 2))
}
// 指定した長さの配列に0を埋め込んで返す
function initArray(array, length){
    for(let i=0; i<array.length; i++){(function(){
        array[i] = new Array(length);
        array[i].fill(0);
        }())
    }
    return array;
}
// 最適解の取得
function getTheBest(routes){
    let min = routes.sort(function(a, b){
        return a.distance - b.distance
    })[0];

    let minCount = routes.filter(function(route){
        return route.distance == min.distance;
    });

    if(minCount.length == routes.length){
        console.log('All routes has same result:', min);
    }else{
        console.log('The minest result is:', min);
    }

    return min
}

実行関数

function main(){
    // 最適解の取得と出力
    let bestRoute = runACO(CITIES);
    console.log('The Best Route is:', bestRoute);
}

main();

実行

nodeで実行。

$node aco_for_tsp.js

結果

loopCount 0
loopCount 1
loopCount 2
loopCount 3
.
.
.
loopCount 99
.
.
.
#代ごとのルート選択結果と距離の一覧が大量に出力されるので省略
#最適解
The Best Route is: { routes:
   [ 0,
     5,
     9,
     7,
     6,
     4,
     2,
     279,
     1,
     278,
     277,
     259,
     260,
     272,
     273,
     261,
     262,
     263,
     266,
     264,
     265,
     137,
     136,
     135,
     133,
     269,
     134,
     268,
     267,
     149,
     177,
     150,
     176,
     175,
     180,
     181,
     182,
     183,
     184,
     186,
     188,
     187,
     165,
     166,
     170,
     171,
     169,
     172,
     106,
     104,
     102,
     107,
     103,
     89,
     105,
     173,
     160,
     159,
     174,
     158,
     118,
     119,
     156,
     157,
     152,
     155,
     151,
     120,
     121,
     122,
     153,
     154,
     30,
     31,
     33,
     32,
     29,
     28,
     27,
     25,
     26,
     21,
     24,
     22,
     23,
     16,
     132,
     130,
     131,
     20,
     19,
     17,
     18,
     129,
     127,
     128,
     126,
     125,
     123,
     124,
     ... 181 more items ],
  distance: 4442.0792898796935 }

結果が全体最適解ではないことがまあまあよくあるので、
あとは蟻の数と代の数とフェロモンの重みや蒸発量などを調整してみたりしてチャレンジすればいいかと思います。

※あくまで練習用なので、もし間違ってるとこがあったら指摘ください。:smile:

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

4.1より前のRedmineでもクリップボードから画像ペタリしたいです(Wikiページに)

これをしたい!

up.gif

Lightshotでクリップボードに保存した画像を Ctl + V だけでWiki記事にペタリ!

↓Lightshotについてはこっち。
ウェ~イなPrintScreenを実現してくれる画面キャプチャソフト『Lightshot』

4.1.0からはできますです

Redmine4.1.0リリース情報 によると標準で搭載されてるようです。

あたいもDockerで4.1.0の環境作ってやってみたら実際できました。

でも宗教上の理由とかで中々アップデートできない事情もありまして…。
というわけで4.0系でもできる方法です。

ビューカスタマイズプラグインにJavaScript

↓ビューカスタマイズプラグインってなんぞや?な時はこっち。
CentOS8にインストールしたRedmineにビューカスタマイズプラグイン導入

作ったビューカスタマイズの設定がこれ↓
image.png

コード
$(function() {

    //実行トリガー
    //ファイル選択してアップロードするボタンがある場所限定ね!
    $('form div.box').has('input:file.filedrop').on('paste', copy_image_from_clipboard);

    //処理のエントリーポイント
    //この機能のコア
    function copy_image_from_clipboard(e) {

        //思わぬ悪影響が出ないように
        if (!$(e.target).hasClass('wiki-edit')) { return; } //Wiki編集画面だけね

        var clipboard_data_ = e.clipboardData || e.originalEvent.clipboardData

        //思わぬ悪影響が出ないように
        if (!clipboard_data_) { return; } //クリップボードが空なら何もしない

        //クリップボードデータ達の中から画像ファイルだけを処理するよ
        var items_ = clipboard_data_.items
        for (var i_ = 0 ; i_ < items_.length ; i_++) {

            var item_ = items_[i_];

            if (item_.type.indexOf("image") == -1) { continue; }

            var blob_ = item_.getAsFile();

            //2021年1月21日19時50分28秒にyukarinさんがpngを貼り付けたなら…
            //20210121_195028_yukarin.png
            var now_date_ = new Date();
            var filename_ = now_date_.getFullYear()
                + ('0' + (now_date_.getMonth()+1)).slice(-2)
                + ('0' + now_date_.getDate()).slice(-2)
                + '_'
                + ('0' + now_date_.getHours()).slice(-2)
                + ('0' + now_date_.getMinutes()).slice(-2)
                + ('0' + now_date_.getSeconds()).slice(-2)
                + '_'
                + ViewCustomize.context.user.login //※1
                + '.' + blob_.name.split('.').pop();

            //※1 ビューカスタマイズプラグイン入れておくと、
            //『ViewCustomize』っていう名前のオブジェクトからログインユーザー情報取れるから便利だよ

            //Redmineの標準JavaScript関数使いながらファイルアップロード
            var file_ = new File([blob_], filename_, {type: blob_.type});
            var input_element_ = $('input:file.filedrop').first();
            handleFileDropEvent.target = e.target; //handleFileDropEvent()はRedmine標準のattachments.jsに
            addFile(input_element_, file_, true); //addFile()はRedmine標準のattachments.jsに

        }
    }

})

『誰がいつ?が分かるように』『複数人が同時に処理しても衝突しないように』を意識してファイル名をちょっと工夫。

蛇足

少しでも使いやすくなるように小さい工夫をコツコツ。

参考サイトさん

https://blog.enjoyxstudy.com/entry/2019/03/14/000000

バージョン

CentOS Linux release 8.3.2011
Redmine 4.0.6.stable
Chrome バージョン: 87.0.4280.141(Official Build) (64 ビット)

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

図解するとVuexストアがはっきり理解できた件、

Vuexストアによる状態管理が十分理解できていなかったが、図解をすることでかなり理解できたからアウトプットしていく。

Vuexストアの図解

(参考記事)
https://codezine.jp/article/detail/11994

Vuex 【図解】.png

【流れの説明】
1. コンポーネントがWebページの更新をするためにアクションを実行する(dispatch)。
2. アクションでWeb APIなどの更新に必要なデータを取得(1)。
3. アクションで取得したデータを状態に反映させるためにミューテーションを実行する(commit)。
4. ミューテーションでステートの変更を行う(Mutate)。
5. ステートでWebページ全体の状態を保持する(3)。
6. 変更されたステートに基づいてコンポーネントが画面を表示(Render)。

細く言えば、まだいろいろな要素が残って行っているけど、基本的にこの流れをおさえておけば、かなり理解できた。

実際に使用してみる

概念だけだとあまり定着できないことの方が多いから手を動かしてみる。

花谷氏の「Nuxt.jsビギナーズガイド」を使って学習した内容をいかして、axios-moduleを利用しWordpressの記事情報を取得するときの状態管理を記述していく。

ヘッドレスのwpの導入方法については今回は割愛。

store/index.js
// 5. ステートでWebページ全体の状態を保持する(3)。
export const state = () => ({
  posts: [],
})

// stateで保持しているデータを取得するために使用
export const getters = {
  posts: (state) => state.posts
}

// 4. ミューテーションでステートの変更を行う(Mutate)。
export const mutations = {
  setPosts(state, { posts }) {
    state.posts = posts
  }
}

// 2. アクションでWeb APIなどの更新に必要なデータを取得(1)。
export const actions = {
  async fetchPosts({ commit }) {
    const posts = await this.$axios.$get('https://~/wp-json/wp/v2/posts?_embed&tags=6')
    // 3. アクションで取得したデータを状態に反映させるためにミューテーションを実行する(commit)。
    commit('setPosts', { posts })
  }
}
pages/index.vue
<template>
  // 6. 変更されたステートに基づいてコンポーネントが画面を表示(Render)。
  <div>{{ posts }}</div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  async asyncData({ store }) {
    // 1. コンポーネントがWebページの更新をするためにアクションを実行する(dispatch)。
    await store.dispatch('fetchPosts')
  },
  computed: {
    ...mapGetters(['posts'])
  },
}
</script>

今回は、直接stateからデータを取得するのではなく、gettersを経由して取得している。

順番がかなり前後しているが、このような流れを意識してVuexストアを記述すると初めてうまくいった。

図解はかなり効果的な定着方法

コードが複雑なればなるほど、頭の中でイメージを膨らませるのでが大切で、頭の中で整理できないものは実際に図などに落とし込みをするのがいいなと感じた。

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

六行テトリス

七行プログラミングとは

かつて、使える文字が「7行×79文字」という制限の元でコーディングの技術を競う、七行プログラミングと呼ばれるショートコーディング技術が流行ったそうです。

その流行の中で生まれたのが、以下に紹介する七行で動くテトリスです。

七行テトリスについて

七行テトリスはネット上で何種類かの亜種が見つかりますが、主だったものを紹介します。

<body id=D onKeyDown=K=event.keyCode-38><script>Z=X=[B=A=12];function Y(){for(C
=[q=c=i=4];f=i--*K;c-=!Z[h+(K+6?p+K:C[i]=p*A-(p/9|0)*145)])p=B[i];for(c?0:K+6?h
+=K:t?B=C:0;i=K=q--;f+=Z[A+p])k=X[p=h+B[q]]=1;if(e=!e)if(h+=A,f|B)for(Z=X,X=[l=
228],B=[[-7,-20,6,h=17,-9,3,3][t=++t%7]-4,0,1,t-6?-A:2];l--;)for(l%A?l-=l%A*!Z[
l]:(P+=k++,c=l+=A);--c>A;)Z[c]=Z[c-A];for(S="";i<240;S+=X[i]|(X[i]=Z[i]|=++i%A<
2|i>228)?i%A?"":"■<br>":"_");D.innerHTML=S+P;Z[5]||setTimeout(Y,99-P)}Y(h=e
=K=t=P=0)</script>
<body id=D onKeyDown=K=event.keyCode-38><script>Z=X=[B=A=12];function Y(){for(C
=[q=c=i=4];f=i--*K;c-=!Z[h+(K+6?p+K:C[i]=p*A-(p/9|0)*145)])p=B[i];for(c?0:K+6?h
+=K:t?B=C:0;i=K=q--;f+=Z[A+p])k=X[p=h+B[q]]=1;h+=A;if(f|B)for(Z=X,X=[l=228],B=[
[-7,-20,6,h=17,-9,3,3][t=++t%7]-4,0,1,t-6?-A:2];l--;)for(l%A?l-=l%A*!Z[l]:(P+=
k++,c=l+=A);--c>A;)Z[c]=Z[c-A];for(S="";i<240;S+=X[i]|(X[i]=Z[i]|=++i%A<2|i>228
)?i%A?"":"■<br>":"_");D.innerHTML=S+P;Z[5]||setTimeout(Y,i-P)}Y(h=K=t=P=0)
</script>

これらのコードをテキストファイルに張り付け、「tetoris.html」といった適当な名前のhtmlファイルにすると、下の画像のようにCUIベースのテトリスを遊ぶことができます。

無題.png

操作は矢印キーで移動、スペースキーで回転です(亜種の中にはEnterキーで回転するバージョンもあります)。

七行テトリスの短縮化

これらのコードについてですが、作成されたのはいずれも十年以上前という、古いコードになっています。
本記事はこれらのコードに対し、令和の今のJavaScriptならさらに文字数を削減できないかを試してみた内容になっています。

注意

  • ロジック部分はそのままです(というか筆者の頭では理解しきれませんでした)
  • あくまでJSの記法をいじって文字数をどうにかしよう、という方針です

七行テトリスからの変更点

その1: event.keyCodeをevent.whichに

キーボードから押下されたキーの取得方法である、event.keyCodeは現在Deprecatedになっており、現在のJSなら代わりにevent.keyやevent.codeを使うべきでしょう。
(参考:JavaScriptのキーボードイベント、キー判定にどれつかう?)
しかし、これらevent.keyやevent.codeは押下されたキーの「文字」を取得するものであり、数値である「キーコード値」を取得するものではありません。
七行テトリスのキーコードの処理は洗練されています。event.keyならevent.keyCodeより文字数を4文字減らせますが、キーコードを扱うためには結局それ以上の文字数がかかりそうです。
ここではevent.keyCodeの代わりに、Deprecatedですが同じくキーコードが取得できる、event.whichを使います。

### 修正前
onKeyDown=K=event.keyCode-38
### 修正後
onKeyDown=K=event.which-38

これで2文字減らせました。

その2: アロー関数を使う

今回最も大きな変更点になります。
function()の代わりに、ES2015から導入されたアロー関数を使うことで、文字数を大きく減らせます

### 修正前
function Y(){for...
### 修正後
Y=()=>{...

当然、const Y=のように変数宣言はせず、グローバル変数として宣言することで文字数を節約します。
また、アロー関数にすることでブロックの末尾に;を加えなければ動かなくなったので追加します。

### 修正前
...}Y(h=K=t=P=0)
### 修正後
...};Y(h=K=t=P=0)

これで12文字→6+1文字と5文字減らせました

その3: brタグをpタグに(※今回は実施せず)

現在のブラウザでは、<p>タグは別に閉じなくても正常に動作します。
そこで、コード内でブロックを描画している箇所で、<br>タグのかわりに<p>タグを使うことにより、文字数を1文字減らせます。

### 修正前
"■<br>"
### 修正後
"■<p>"

しかしこれをやってしまうと、テトリスとして動作はするものの、画面全体が大きく縦に間延びしてしまうため、今回は見送りました

無題2.png

結果(479バイト)

上記2点の改良を加えたテトリスが以下の通り。6行に収めることができました。
文字数は476(全角文字を含む)、バイト数は479バイトとなります。

<body id=D onKeyDown=K=event.which-38><script>Z=X=[B=A=12];Y=()=>{for(C=[q=c=i=4];
f=i--*K;c-=!Z[h+(K+6?p+K:C[i]=p*A-(p/9|0)*145)])p=B[i];for(c?0:K+6?h+=K:t?B=C:0
;i=K=q--;f+=Z[A+p])k=X[p=h+B[q]]=1;h+=A;if(f|B)for(Z=X,X=[l=228],B=[[-7,-20,6,h
=17,-9,3,3][t=++t%7]-4,0,1,t-6?-A:2];l--;)for(l%A?l-=l%A*!Z[l]:(P+=k++,c=l+=A);
--c>A;)Z[c]=Z[c-A];for(S="";i<240;S+=X[i]|(X[i]=Z[i]|=++i%A<2|i>228)?i%A?"":
"■<p>":"_");D.innerHTML=S+P;Z[5]||setTimeout(Y,i-P)};Y(h=K=t=P=0)</script>

その他修正箇所

ロジック部分を除けば、他にはonKeyDown,innerHTML,setTimeout,<script>~</script>などの文言が目につきますが、これ以上JSの記法を書き換えてコードを短縮化することは難しいのではないかと思います。

  • setTimeoutの代わりにrequestAnimationFrameが使えるかもしれませんが、文字数が増えるだけです
  • <script>タグはもちろん必要ですし、閉じタグ</script>は無ければ動作しません

そのため、これ以上短縮化するにはロジック部分の改良が必要になるかと思います。が、それは他の方に譲りたいと思います。

参考文献

参考:たった7行でテトリスを実装「七行プログラミング」とは
参考:482バイトテトリス
参考:JavaScriptのキーボードイベント、キー判定にどれつかう?

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

Next.jsの環境構築とメリット

参考

vercel社が提供するNext.js公式チュートリアル引用でございます。まるパクリです。
また、YouTubeで発信されているトラハックさんの動画がNext.jsを学習する上でめちゃくちゃ分かりやすかったです!!
誠に勝手ながら紹介させていただきました。

Next.jsで環境構築を行うメリット

1.Babel+Webpackの複雑な環境設定が不要
2.Code Splittingのような最適化設定が不要
3.パフォーマンスやSEOのためのpre-render設定が不要
4.Renderingのタイミングを選択できる(CSR,SSR,SSG)
5.サーバーサイドの処理(api)を簡単に実装できる

Next.js環境構築

※注意
- Windowsの方はUnixコマンドを使えるgit bashを用いてください。
- nodeが入っている前提です

$ npx create-next-app nextjs-blog --use-npm --example "https://github.com/vercel/next-learn-starter/tree/master/learn-starter"

succesfully!でプロジェクトができたら、

$ cd nextjs-blog(プロジェクト名)

$ npm run dev

ローカルサーバーを立ち上げ
http://localhost:3000/
を開く

終わりに

Next.jsのコミュニティは活発でアップデートも盛んなところが面白い!!:relaxed:

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

jsとhtmlでメモ化を実践

お世話になっている本
もっとプログラマ脳を鍛える数学パズル(増井 敏克 (著))
です。ありがとうございます。

今回はその中の序章、メモ化を利用して席に座れる人数のMax値と人数を入力すると席に座る人が2人以上である座り方の組み合わせの数を教えてくれるwebアプリを作りました。
大人数で行ったときに便利ですね。(便利ではない)

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <title>sample1</title>
</head>
<body>
  <form id="form1" action="#">
    <p>人数</p>
    <input type="text" id="input_message" value="">
    <p>テーブルmax</p>
    <input type="text" id="input_message2" value="">
    <input type="button" onclick="func1()" value="送信">
  </form>
  <div id="output_message"></div>
  <script language="javascript" type="text/javascript">
  function func1() {

    var N = document.getElementById("input_message").value;
    input = check(N,2);
    document.getElementById("output_message").innerHTML = input;
    //input = String(check(N,2,M));
  }


  var memo = {};
  function check(remain,pre){
    var M = document.getElementById("input_message2").value;
    //console.log("remain,pre:"+ remain,pre);
    if(memo[[remain,pre]]) return memo[[remain,pre]];

    if(remain < 0) return 0;
    if(remain == 0) return 1;

    var cnt = 0;
    for(var i =pre; i <= M; i++){
          cnt += check(remain -i,i);
          //console.log("cnt"+ cnt);
        }

    return memo[[remain,pre]] = cnt;
}
    func1();
  </script>
</body>
</html>

jsはしっかりと勉強したことはなかったのですが、変数の宣言とかがないのは楽だけどfunc1()みたいに言わないと実行しないことにずっと気づけなかったりしたのがちょっと大変だった。
メモ化っていうのは、実行時間を早めたり無駄を省くみたいでいろいろいじくりながら理解できた?
まあでもめちゃくちゃ回帰しててイメージとしては、組み合わせを調べるためにツリーを全部書いてるような気分。
もう一つ二重配列で考えるのがあってそっちの方が感覚的にはわかりやすかった。

あとはhtmlとくっつけるのが意外と大変。web系はあんまり興味なかったから。でも調べながらなんとかできて先人様様ありがとうございます。という感じです。

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

LIFF URLでクエリパラメータを使おうとするとliff.initが正常に動かなかった話

概要

業務の中でLIFF URL(https://liff.line.me で始まるURL)にクエリパラメータを付与して処理を行う、というケースがありました。
そこで困ったことがあったので共有しようと思った次第です。

結論

LIFF URLにクエリパラメータを付ける場合、liff.initのあとにsetTimeoutなどのsleep処理を入れると動作する。(根本原因は不明です、、)

動作環境

LIFF: v2.4.1
クライアント: Nuxt.js(SPAモード)

LIFFでクエリパラメータを使えるようになった

LIFF v2.3.0からhttps://liff.line.me?hoge=hoge のようにLIFF URLに対してクエリパラメータを使えるようになりました。
詳しくは以下をご覧ください。
https://developers.line.biz/ja/docs/liff/opening-liff-app/

困る前のクライアントのソース

liffのSDKを使用するには「liff.init」という関数を実行する必要があります。
今回はNuxtを使用していたので、pluginsにliff.initを実行する関数を書いて実行しました。
この段階では正常に動作することが確認されました。

plugins/liffConfig.js
import liff from '@line/liff'
const liffConfig = async (_) => {

  await liff.init( { liffId:'xxxxxxxxxxxx' } )
}

export default liffConfig

困ったこと

要件の都合上、LIFF URLに対してクエリパラメータを付与してアクセスする必要があったため、LINE DevelopersコンソールでLIFF URLを作成しhttps://liff.line.me?hoge=hoge の形でアクセスしました。
するとliff.init前後の処理が無限ループされる現象が起きました。

原因を探す

liffのSDKはローカルホストでは動作しないため、chromeのデベロッパーツール等でデバッグすることが出来ません(ここが一番つらかった)。
そのため開発環境にひたすらalert関数を入れて、どこまで処理が走っているのか検証しました。

原因を発見(?)

alert関数を入れて打鍵しながらデバッグした結果、liff.init後のalertで数秒待ったのちに処理を続けると、以前と同様に動作することが確認できました。

plugins/liffConfig.js
import liff from '@line/liff'
const liffConfig = async (_) => {

  alert('initする前')
  await liff.init( { liffId:'xxxxxxxxxxxx' } )
  // 打鍵時にここで数秒待つと動いた、、、
  alert('initした後')
}

export default liffConfig

最終的に

数秒待ったのちに動作するのであれば、そのような処理を追加したら良いのでは?と思い、setTimeoutを使用してsleep処理を追加しました。
根本原因は不明ですが、動くものを目指すという意味ではとりあえず良しとしました。

plugins/liffConfig.js
import liff from '@line/liff'
const liffConfig = async (_) => {

  await liff.init( { liffId:'xxxxxxxxxxxx' } )
  await sleep(1000)
}
// setTimeoutをそのまま使うのはイケてないと思ったので、Promiseでラップしています。
const sleep = (msec) =>
  new Promise((resolve) => setTimeout(resolve, msec))

export default liffConfig

最後に

根本原因を突き止めれなかったのは悔しいですが、何とか動くものが出来て良かったのかなと思います。
少しでも同じ現象に出くわした方の助けになれれば幸いです。
こうしたほうが良いのではなどのコメントがあれば、ぜひよろしくお願いします!

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

[Vue3] v-onでKeyCodeが非推奨になりました

https://v3.vuejs.org/guide/migration/keycode-modifiers.html

Vue2系では

<input @keyup.13="submit" />

のようにキーコードを指定してメソッドを発火させることができたのですが
同じことをVue3系でやろうとすると

'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. 
Using 'KeyboardEvent.key' instead  vue/no-deprecated-v-on-number-modifiers

Lintの設定によってはエラーになってしまいます。
これはkeyCodeが非推奨になったためです。

Vue3系ではキーコードを直接使用せずにエイリアスを使うことが推奨されています。
先程の例だとkeyCodeの13はenterの入力なので
以下のように書いてあげるとLintで怒られなくなります

<input @keyup.enter="submit" />

ちなみにエイリアスの記法はケバブケースになります

// GOOD
<input @keyup.page-down="submit" />

// BAD
<input @keyup.pageDown="submit" />

input type="number"でeを入力できないようにする

inputタグで数字だけを入力させたいためtypeをnumberにしましたが指数表記のeも入力できてしまいます

Vue2系であればeのキーコード69をpreventでもLintに怒られませんでしたがVue3では怒られてしまいます

<input type="number" @keydown.69.prevent>

そのため69ではなくeを使用するかメソッド内で数字を判定してあげる必要があります

<input type="number" @keydown.e.prevent>

// もしくは

<input type="number" @keydown="checkNumber">
...
methods: {
  checkNumber (input) {
    if (!input.key.match(/[0-9]/)) input.preventDefault()
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[TailwindCSS] Error: PostCSS plugin tailwindcss requires PostCSS 8.

エラー発生状況

Gatsbyで制作を進めていたサイトにTailwindを導入しようとした(21年1月時点)

PostCSS plugin postcss-nested requires PostCSS 8.
Migration guide for end-users:
https://github.com/postcss/postcss/wiki/PostCSS-8-for-end-users

対処

一度アンインストールして、互換性ビルドを再インストール

npm uninstall tailwindcss postcss autoprefixer
npm install tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

原因

PostCSS8対応が追いついていないという感じでしょうか。
サポートされたらlatest使ってくれとのこと。

参照

PostCSS 7 compatibility build
PostCSS plugin postcss-nested requires PostCSS 8 #2799

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

axios では xhr? が効かない

自分用メモ。

axiosはAjaxで通信するときに、リクエストヘッダーにX-Requested-Withを付けません。

参照: https://github.com/axios/axios/issues/1322

なので、Railsのxhr?メソッドは偽になります。

if request.xhr?
 # axiosだと効かない
end

xhr?メソッドを使いたければ、JavaScript側でaxiosのヘッダーにX-Requested-Withを追加する設定が必要です。

Axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

あるいは、Rails側でX-Requested-Withがなくても動くようにします。

if params[:format] == 'json'
 # Ajaxの場合の処理
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sam local start-apiでヘッダーが勝手にキャメルケースになるんだが?

表題の通り、sam local start-apiでヘッダーが勝手にキャメルケースになる問題についてです。

こちらにドンピシャの回答があるのですが、内部で使っているFlaskの仕様だそうです。
Headers are received in Camel-Case · Issue #1860 · aws/aws-sam-cli

仕方ないのでNode.jsでは下記ワークアラウンドをしてヘッダーをすべて小文字にして対応しましょう。
(Pythonならissueのコメントにサンプルコードが載っています)

const toLowerCaseKey = function(object) {
    let newObject = {};
    for (let key in object) {
        if (object.hasOwnProperty(key)) {
            newObject[key.toLocaleLowerCase()] = object[key];
        }
    }
    return newObject
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Uncaught Error: Cannot find moduleが出た話

こんにちは。
現在JavaScriptを勉強中です。
今回はUncaught Errorに出くわしたのでその話をしたいと思います。

結論から申し上げますと、すごくしょうもない理由でした(笑)

経緯

Ruby on Rails アプリケーションにでJavaScriptを実装している段階で、すでに作成されているapplication.jsに読み込ませるJavaScriptファイルを記述しました。
ローカルサーバーにて、挙動を確認しようとしたところ、下記のようなエラーが出ました。

application.js:10 Uncaught Error: Cannot find module '../sample.js'
    at webpackMissingModule (application.js:10)
    at Object../app/javascript/packs/application.js (application.js:10)
    at __webpack_require__ (bootstrap:19)
    at bootstrap:83
    at bootstrap:83
webpackMissingModule @ application.js:10
./app/javascript/packs/application.js @ application.js:10
__webpack_require__ @ bootstrap:19
(anonymous) @ bootstrap:83
(anonymous) @ bootstrap:83

仮説

①webpackMissingModuleとあるので、またgem関連でエラーが生じているのかなと思い、bundle installを実施するも改善なし。
②読み込ませたいJavaScriptファイル内の記述がおかしいのかなとも考えましたが、そもそも読み込まないエラーなのであればその線は無い・・・。
③エラー内容をよく見ると、webpackMissingModuleの後に、(application.js:10)とあり、application.jsの10行目がおかしいのでは?と捉えられる・・・。

解消方法

結論としては、読み込ませるJavaScriptファイルの配置ミスでした。

application.jsの10行目を見ると、私はこのように記述をしていました。

require("../sample")

それに対して、実際のsample.jsファイルはこのような配置になっておりました。

app
 javascript
  channels
  packs
   sample.js

ファイル配置の書き方、見にくかったらすみません・・・。

上記のようにpacksのなかにsample.jsが存在しており、かつapplication.js内での読み込ませる記述が("../sample")となっていたため、今回のエラーが発生したようです。

したがって、sample.jsファイルを1つ上位ディレクトリ、javascriptディレクトリ直下に配置し直したところ、エラーは改善されました。

考察

今回私は何も考えずにパスの指定を

require("../sample")

としてしまいました。

このパスの指定における .(ドット)と..(ドットドット)の意味をしっかり把握しておりませんでした。

.(ドット)→現在のディレクトリを表す。今回の場合であれば、application.jsが存在するディレクトリ、packsになります。
..(ドットドット)→現在のディレクトリより1つ上位のディレクトリを表す。今回の場合、application.jsが存在するディレクトリの1つ上位、javascriptディレクトリになります。

パスの指定というのは色々複雑で混乱してしまいますね。
とてもしょうもない理由で発生したエラーでしたが、誰かの参考になればとても幸いです。

ご覧いただきありがとうございました!

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

JavascriptでSlackbotにリアクションをさせよう

本記事はJavaScriptでSlackbotにリアクションさせる方法を紹介するものです。
リアクションする

Pythonでリアクションをさせる方法については「Slackbotにリアクションをさせる〜PythonでのBot開発〜」をお読みください。

Slackの絵文字リアクション機能について

Slackの機能で自分や他の人に投稿したコメントに対してマークをつけることができます。
リアクションのマークをつけることで相手に同意や反対の意思を示すことが可能です。

下記のように一つのコメントに対して複数のリアクションをすることも可能です。
昼はカレーかラーメンか

JavaScriptでSlackbotにリアクションをさせる方法

◆Botのリアクションの記述

JavaScriptでSlackbotにリアクションさせる場合の記述例は以下の通りです。

controller.hears(['ご機嫌いかが'], 'direct_message,direct_mention,mention', function(bot, message) {

    // nameに絵文字を指定する
    bot.api.reactions.add({
        timestamp: message.ts,
        channel: message.channel,
        name: 'smile',
    }, function(err, res) {
        if (err) {
            bot.botkit.log('Failed to add emoji reaction :(', err);
        }
    });
});

◆実行結果
リアクションする

このままだとかなり長いので関数化しておきましょう。

controller.hears(['ご機嫌いかが'], 'direct_message,direct_mention,mention', function(bot, message) {
    bot_react(bot, message, 'smile')
});

function bot_react(bot, message, emoji) {
    bot.api.reactions.add({
        timestamp: message.ts,
        channel: message.channel,
        name: emoji,
    }, function(err, res) {
        if (err) {
            bot.botkit.log('Failed to add emoji reaction :(', err);
        }
    });
}

1度に複数のリアクションをさせることも可能です。

controller.hears(['ご機嫌いかが'], 'direct_message,direct_mention,mention', function(bot, message) {
    bot_react(bot, message, 'smile')
    bot_react(bot, message, 'robot_face')
});

◆実行結果
リアクションを2つする

絵文字について

Slack絵文字の代替文字を指定します。
代替文字は以下の画面の下に表示されている「::」で囲まれている文字です。

リアクション一覧

Slack内から打つ場合は、「:スマイル:」など日本語での記述も可能ですが、Botにリアクションさせる場合はNGです。

◆この記述はエラーとなる

@respond_to('^ご機嫌いかが?$')
def question_1(message):
    # 日本語はNG
    message.react('スマイル')

以上、JavaScriptでBotのリアクションをさせる方法についてでした。

◆関連記事
SlackbotをJavaScriptで作成しよう
Slackbotにリアクションをさせる(Python)

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

フロントエンド目線でのObserverパターン

デザインパターンの一つであるObserverパターンについて学習したので、フロントエンド目線での所感をまとめてみます。

Observerパターンとは

観察する側(Observer)、観察される側(Subject)に処理を分割し、Subjectの状態が変化した際にObserverに通知し、通知毎に定義した処理を実行するパターンです。フロントエンド界隈においては、JavaScriptのaddEventListenerとdispatchEvent、jQueryのonとtriggerをイメージするとわかりやすいかもしれません。

どういった時に使うと良さそうか

Observerパターンを使うことによってObserverとSubject間の処理が疎結合になります。
Subjectは状態によってObserverに通知することが主な目的となり、Observerがどのような処理をしているのか関心がありません。
またObserverはSubjectが通知した際に必要な処理を実行することが主な目的となり、Subjectの状態がいつどのように変化するのか関心がありません。お互いの関心が薄くなり疎結合になります。

フロントエンドではユーザー操作、API通信結果など状態によってUIを変更するといったことが多々あるかと思いますが、状態管理とUI変更の処理を分離したいとなった場合にObserverのパターンが上手くハマりそうです。
(状態管理 → Subject, UI変更 → Observer)

SubjectとObserverの関係は1対多の関係であり、Observerは後からいくらでも追加することができます。
状態が変化した際に更新するUIが膨大に存在している、今後どんどん増えることが予想されるといった状況であればObserverパターンを導入することによって、拡張性に優れた保守しやすいコードを記載することが可能だと思います。

また副産物として状態管理とUI変更を分離することにより、テストコードを記載しやすくなるメリットもあるかと思いました。
Jestなどテスティングフレームワークを使用し、フロントエンドのコードをユニットテストする際、UI変更のユニットテストをするには労力がかかりますが、DOM操作を必要としない状態管理のみのテストは比較的に労力がかかりません。
Observerパターンを導入することにより、テストしやすい箇所・しにくい箇所を分離し、しやすい箇所に対して進んでテストコードを記載していくといったことが可能です。

具体的には下記のようなUIを作る際にObserverのパターンを導入すると良さそうです。

  • ファイルダウンロードとインジケータ
    • ダウンロードの状態(Subject)によってインジケータ(Observer)を更新する
  • API通信とエラーメッセージ表示
    • API通信と状態(Subject)によってエラーメッセージ(Observer)を更新する

サンプルコード

API通信、UI変更を1ファイルで記載したものをObserverパターンに合わせて分割してみたいと思います。長くなるためUI変更の詳細なコードは割愛します。

1ファイルにまとめたコードのサンプル

const fetchProductList = async () => {
  try {
    const response = await axios.get("https://***");

    if (response.status === 200) {
      // 商品情報リストを生成しHTMLに挿入する
      console.log("success");
      return;
    }
  } catch (e) {
    if (e.response.status === 403) {
      // ステータスコード403時のエラーメッセージの表示
      console.log("error 403");
      return;
    }

    if (e.response.status === 500) {
      // ステータスコード500時のエラーメッセージの表示
      console.log("error 500");
      return;
    }
  }
};

fetchProductList();

SubjectとObserverのインターフェースを作成しimplementsする形で実装していきます。
Observerのインスタンスを生成後、Subjectの addObserver メソッドを呼び出すことによってObserverの参照を保持しておき、必要に応じて notify メソッドを呼び出し全Observerに通知します。

type EventType = "success" | "error"
type NotifyParameter = {
  eventType: EventType,
  statusCode: number,
  data?: [string: any]
}
interface Subject {
  addObserver: (observer: Observer) => void;
  notify: (params: NotifyParameter) => void;
}
interface Observer {
  update: (params: NotifyParameter) => void;
}

実装したSubjectは下記になります。
通信が成功・失敗した際に notify メソッドを呼び出していますが、成功・失敗後の具体的な処理は記載していません。

class ProductListSubject implements Subject {
  private observers: Observer[] = []

  public addObserver(observer: Observer) {
    this.observers.push(observer);
  }

  public notify(params: NotifyParameter) {
    this.observers.forEach(observer => {
      observer.update(params);
    });
  }

  public async fetch() {
    try {
      const response = await axios.get("https://***");
      this.notify({
        eventType: "success",
        statusCode: response.status,
        data: response.data
      })
    } catch(e) {
      this.notify({
        eventType: "error",
        statusCode: e.response.status
      });
    }
  }
}

実装したObserverは下記になります。
Observerはいつ実行されるのかの関心はありません。update メソッドを実行した際に必要な処理を記載しているのみです。

class ProductListUpdateObserver implements Observer {
  public update(params: NotifyParameter) {
    if (params.statusCode === 200) {
      console.log("success! response data: ", params.data);
      // 商品情報リストを生成しHTMLに挿入する
    }
  }
}

class ErrorMessageObserver implements Observer {
  public update(params: NotifyParameter) {
    if (params.statusCode === 500) {
      console.log("error! statusCode: ", params.statusCode);
      // ステータスコード500時のエラーメッセージの表示
    }
    if (params.statusCode === 403) {
      console.log("error! statusCode: ", params.statusCode);
      // ステータスコード403時のエラーメッセージの表示
    }
  }
}

Subjectのインスタンスを生成後、Observerを登録しSubjectの fetch メソッドを呼び出します。
API通信後、成功・失敗に応じて notify メソッドが呼び出され、Observerの update メソッドが呼び出されます。

const execute = () => {
  const subject = new ProductListSubject();
  subject.addObserver(new ProductListUpdateObserver());
  subject.addObserver(new ErrorMessageObserver());

  subject.fetch();
}

execute();

API通信後にUI変更の処理を増やしたいとなった際はObserserを新しく作成し addObserver を呼び出すだけです。最終的なコード量は分割前より多くなっていますが、拡張性に優れている & 保守しやすいコードになっていると思います。

コードを書いてから思いましたが、通知処理にEventEmitterを使うとなお良いかもしれません。
EventEmitterを使用する場合、任意のイベントを複数emitできるため、statusCode毎に別々のイベントをemitすればObserver内のstatusCodeごとの分岐を削除できそうです。

おわり

参考資料

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

【Elixir/Phoenixプロダクトへリプレイス②】フロント部分:JavaScriptをTypeScriptにリプレイスする例

概要

こちらの 「Elixir/PhoenixでTypeScriptを使えるようにする。」を実装したPhoenixプロダクトのJavaScript部分をTypeScriptにリプレイスする手順を残しておく。

手順

リプレイス前のjsファイルをfilename.jsとする。

1. filename.jsをassets/js配下に配置

JSファイルをコピーしてペースト

2. ファイル名filename.jsをfilename.tsに変更

3. html部分を書き換える

filename.html.eex

-<a href="javascript:void(0);" onclick="CallScript();">JS呼び出す</a><
+<a href="javascript:void(0);" id="call_script">TS呼び出す</a>

4.filename.tsをTypeScriptに書き換える

filename.ts

+class Filename {
+    constructor() {
+        let call_script = document.getElementById("call_script");
+        call_script!.addEventListener("click", (e: Event) => this.CallScript());
+    }
+
-function CallScript() {
-  console.log("OK");
-};
+    CallScript() {
+        console.log("OK");
+    }
+}
+new SayonalaJa();

5.webpack.config.jsに記述

    entry: {
      'app': './js/app.js',
+     'filename': './js/filename.ts',
    },

6.layout部分でsrcを読み込む実装を追記

例えば、app.html.eexで読み込む。

    <script src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
+   <script src="<%= Routes.static_path(@conn, "/js/filename_ja.js") %>"></script>
  </body>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsのmodule.exportsの仕組みを理解する

はじめに

こちらは、エンジニアの新たな学びキャンペーンに向けた記事となります。

Node.js + Express で作る Webアプリケーション 実践講座を参考にしながら、
module.exportsについて理解を深めたことを記事にします。

実行環境

  • Node.js v12.16.3
  • Express 4.16.1
  • 10.4.11-MariaDB

本記事の概要

私は今現在、Webアプリケーションの開発をしていますが、ある問題を抱えています。
その問題とは、1つのファイル内のソースコードが膨らみすぎて可読性が落ちているというものです。
これはデータベースへの接続、ミドルウェアの呼び出し、レスポンスの処理内容など、あらゆる情報をapp.js内に書き込んでいることに由来します。

そこで、app.js内のソースコードを削減することを目的に、
データベース接続を行う関数を別ファイルdbConnect.jsに記述し、
module.exportsを使ってそれをapp.jsから呼び出すようにリファクタリングしました。

本記事では、はじめにmodule.exportsについて例題を用いて解説し、
その後で応用として、データベース接続用の関数をmodule.exportsを使って呼び出します。

対象のUdemy講座で学んだこと

対象の講座で学んだことのうち、特に本記事へと反映する内容は以下となります。

  • module.exportの使い方
  • requireの使い方

module.exportsとは

JavaScript(Node.js)において、
あるファイルに存在する変数や関数を、別のファイルで実行する機能です。

似たようなものにexportsがありますが、本記事では触れません。

(例) sub.js内の関数をmain.jsで実行する

sub.jsで宣言した変数をmain.jsで実行します。

(*varletではなくconstで宣言しているので、厳密には変数ではなく定数です。)

main.js
const foo = require("./sub.js");// sub.jsからmodule.exportsで指定した変数を読み込み、変数fooに代入する

foo("bee");

// bee
sub.js
const hoge = function(bar){
    console.log(bar);
}

module.exports = hoge;  // main.jsからrequireされたら、hogeという変数を渡す

(実践) データベース接続用の関数を、別ファイルに配置する

app.jsにすべてのソースコードを書き込んでいた状態から、
app.jsdbConnect.jsの2つにソースコードを分割します。

リファクタリング前のソースコード

app.js
const express = require("express");
const app = express();
const mysql = require("mysql");
const dbcn = mysql.createConnection({
    host:"localhost",
    user:"root",
    password:"1234",
    database:"idea"
})
app.set("view engine", "ejs");

app.get("/", (req, res)=>{
    const sql = "select * from ideatext";
    dbcn.query(sql, function (err, result, field){
        if(err) throw err;
        console.log(result);
        res.render("index", {idea : result});
    })
})

app.listen(3000);

リファクタリング後のソースコード

app.js
const express = require("express");
const app = express();
const mysql = require("mysql");
const dbcn = require("./dbConnect.js")
const connection = dbcn.dbcn;

app.set("view engine", "ejs");

app.get("/", (req, res)=>{
    const sql = "select * from ideatext";
    dbcn.query(sql, function (err, result, field){
        if(err) throw err;
        console.log(result);
        res.render("index", {idea : result});
    })
})

app.listen(3000);
dbConnect.js
const mysql = require("mysql");

const dbcn = mysql.createConnection({
    host:"localhost",
    user:"root",
    password:"1234",
    database:"idea"
})

module.exports= dbcn;

参考

Node.js + Express で作る Webアプリケーション 実践講座

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

[Javascript] 配列の主な操作をまとめてみた

はじめに

Javascriptの配列の操作方法について調べてみたので備忘録として残す

配列を操作するメソッド

※配列を作成

下記の配列をもとに進める

let test = ['a', 'b']

console.log(test.length)
// 2

位置を指定して配列にアクセスする

test[0]
// a

test[fruits.length - 1]
// b

配列に値を追加

test.push('c')
// ['a', 'b', 'c']

配列に末尾を削除

test.pop()
// ['a', 'b']

配列に銭湯に値を追加

test.unshift('z')
// ['z', 'a', 'b']

要素の添字を取得

test.indexOf('a')
// 1

要素の添字を取得

test.indexOf('a')
// 1

配列をコピー

let testCopy = test.slice()
// ['z', 'a', 'b']

配列の長さを取得

test.length
// 3

配列を逆に入れ替え

test.reverse()
// ['b', 'a', 'z']

参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array

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

MQTTによるIoT制御を無料でやってみる

前置き

※途中まで頑張りましたが、断念したため供養記事となります。m..m

背景

IoTの制御としてArduino, Raspberry Piなどを用いて各種センサー情報を収集、又はアクチュエータの制御などを想定する。

MQTTとは

  • ネットワークプロトコルの一種
  • HTTPと比較すると以下が特徴的
  • 一対多, 多対多で情報のやりとりが可能
  • リアルタイムでの通信が可能
  • ヘッダー情報が少なくHTTPの10分の1と軽量 など

詳細はIBMのサイトに記載されています。
MQTT の基本知識

基本的に Subscriber(受け手), Publisher(送り手), Broker(中継点)の3者で構成します。
base.png

Brokerにはtopicと呼ばれるパスを設定し、Sub,Pub共に共通のtopicで送受信を行います。
例: Publish =(topic: hoge)=> Subscribe
topicはhoge/bar, hoge/hoge など"/"を用いて階層化することができます。
topicを分けることで任意のクライアント同士で情報のやり取りを行います。
topic.png

MQTTを利用するにあたってはBrokerを構築する必要があります。
手早く無料で利用するには(制約がありますが)いくつかのサービスが利用できます。

構成図

今回は Beebotte と Heroku を組み合わせた場合を紹介します。

  • Beebotte を利用するメリット
    • 接続台数が無制限
    • メッセージが一日あたり5万件送信できる
    • メッセージを一日あたり5千件保存できる
    • SSL接続が可能
  • デメリット
    • 中継点にアプリケーションが乗せられない
    • topicの管理がやや複雑(channel / resource の形。生成、削除にAPIを叩く必要がある) 接続台数と送信可能件数が魅力的ですが、BrokerにDBを接続したり、メッセージによって特定の処理を行いたい場合は工夫が必要です。

そこでHerokuをSubscriber, PublisherとしてBeebotteに接続することで擬似的に実現します。
BeebotteWithHeroku.png

HerokuはPaaSなのでBrokerとして利用できますが、無料だと5台までしか接続できません。

実装

環境

  • Mac
  • node.js v8.12.0 (npm v6.4.1)
  • git v2.14.3 (Apple Git-98)

環境構築

Brokerとトピックの開設

  1. Beebotte のサイトでアカウント登録
  2. Control Panel > Channels > Create New
  3. Channel Name, Description と Resource Name, Description を入力し Create channel
    • 今回の例: ChannelName: TestCh, ResourceName: Hoge
  4. Channels > Channel Name > Channel Token を確認(チャンネル毎に別)
  5. Control Panel > Account Settings > Access Management > API Key, Secret Key を確認

Beebotteの場合 トピック名は "ChannelName / ResourceName" です

MQTT クライアントの実装

Beebotteからライブラリが提供されている他、MQTT.jsや他の言語のライブラリ1でも可能です。
Tutorialsから実装例が紹介されています
今回はMQTT.jsを用いた例を紹介します

プロジェクトを作成するディレクトリで

npm install mqtt --save

Subscriber

メッセージを受信する
トピックを開設した際に確認したChannel Token と トピック名を書き換えてください

subscriber.js
const mqtt = require('mqtt')
const CHANNEL_TOKEN = '*********************'

const client = mqtt.connect('mqtt://mqtt.beebotte.com',
  {username: 'token:' + CHANNEL_TOKEN, password: ''} 
)

const onMessageCallback = (topic, message) => {
  console.log(topic + '   ' + message)
}

client.on('message', onMessageCallback)
client.subscribe('TestCh/Hoge')

Publisher

メッセージを発信する
インターバルで定期的に送ってます。

publisher.js
const mqtt = require('mqtt')

const CHANNEL_TOKEN = '*********************'

const client = mqtt.connect('mqtt://mqtt.beebotte.com',
  {username: 'token:' + CHANNEL_TOKEN, password: ''} 
);

setInterval(function() {
  const message = 'Hello, world!'
  client.publish('TestCh/Hoge', message)
}, 1000);

2つのターミナルで動作確認してみます

node publisher.js
node subscriber.js
#TestCh/Hoge   Hello, world!
#TestCh/Hoge   Hello, world!
#...

メッセージが表示されていればうまく送信、受信できています。

Heroku 編

志半ばで筆を投げました。すみません。

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