20200117のJavaScriptに関する記事は23件です。

初心者による ES6 関数

複数の戻り値を個別の変数に代入する

「関数から複数の値を返す」ためには、配列/オブジェクトとして値を一つにまとめる方法があります。
そのような場合には、

sample.js
function getMaxMin(...nums) {
 //配列で返す
 return [Math.Max(...nums), Math.Min(...nums)]
}

//分割代入
let [Max, Min] = getMaxMin(1,2,3,4,5)
//Maxがいらない場合(最大値は切り捨てられる)
let [, Min]

タグ付きテンプレート文字列

変数を埋め込む際に「<」「>」などの文字列を「&lt」「&gt」に置き換えたい(エスケープしたい)ときに、役立つのがタグ付きテンプレート文字列です。
タグ付きテンプレート文字列の実態は、単なる関数呼び出しでしかありません。

関数名`テンプレート文字列`

の形式で呼び出されます。タグ付きテンプレート文字列で利用するためには、関数は次のような条件を満たしておく必要があります。

1. 引数として「テンプレート文字列(分解したもの)」と「埋め込み変数(可変長引数)」を受け取る
2. 戻り値として加工済みの文字列を返すこと

たとえば、

sample.js
//エスケープ処理
function escapeHTML(str) {
 if(!str) {return ''}
 str = str.replace(/&/g, '&amp')
 str = str.replace(/</g, '&lt')
 str = str.replace(/>/g, '&gt')
 str = str.replace(/"/g, '&quot')
 str = str.replace(/'/g, '&#39')
 return str
}

//タグ付きテンプレート文字列
function e(templates, ...values) {
 let result = ''
 for(let i = 0, len = templates.length; i < len; i++) {
  result += templates[i] + escapHTML(values[i])
 }
 return result
}

let name = '<"Satou" & \'Saitou\'>'
console.log(e`こんにちは、${name}達!`)

クロージャ

クロージャとは、「ローカル変数を参照している関数ない関数」のことである。

たとえば、

sample.js
function closure(init) {
 let counter = init

 return function() {
  return ++counter
 }

let myClosure = closure(1)
conosle.log(myCLosure()) //2
conosle.log(myCLosure()) //3
conosle.log(myCLosure()) //4
}

一見するとclosure関数は「初期値としてinitを受け取り、それをインクリメントした結果を返している」よに見えます。しかしよく見ると、戻り値は数値ではなく「関数」になっていることがわかります。
この匿名関数を返しているということによって、closure関数の終了後もローカル変数counterは保持されるため「一種の記憶域を提供するしくみ」と考えることができる。

よってこのようなことも可能である。

sample.js
function closure(init) {
 let counter = init

 return function() {
  return ++counter
 }

let myClosure1 = closure(1)
let myClosure2 = closure(100)

conosle.log(myCLosure1()) //2
conosle.log(myCLosure2()) //101
conosle.log(myCLosure1()) //3
conosle.log(myCLosure2()) //102
}

closure関数の同じcounter変数を参照しているのにも関わらず、異なる変数を参照しているかのようにふるまうのである。ここではスコープチェーンという考え方が用いられており、javascriptでは、このスコープチェーンの先頭つまり最内側の関数に位置するオブジェクトから順にプロパティを参照するため、もともとが同じ変数であっても違う値を個々に保持している現象があってもおかしくないのである。

参考資料

山田祥寛様 「javascript本格入門」

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

JSON.Stringifyで循環オブジェクト参照構造体が処理できないのをcycle.jsで処理した

概要

https://qiita.com/saitoeku3/items/9e9a608e53029d541a8f
と同じエラーにあったので、cycle.jsで処理したサンプルコードを紹介するよというお話

cycle.js

https://github.com/douglascrockford/JSON-js/blob/master/cycle.js

インストール

npm install json-cyclic

サンプルコード

//import
const decycle = require('json-decycle').decycle;
const retrocycle = require('json-decycle').retrocycle;
...

...
//文字列に変換
receiveNewAcString = JSON.stringify(receiveNewJson, decycle());

//JSONオブジェクトに変換
receiveNewAc = JSON.parse(receiveNewAcString, retrocycle()).data;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueとFirebaseと使ってAtCoderのAC数をツイートするウェブアプリケーションを作った

概要

AtCoder上でのAC数を1日1回ログインしたツイッターアカウントでツイートしてくれるdevotterというアプリケーションを作成したよというお話

リンク

スライド

Github

ソースコード(フロントエンド)

<template>
  <v-container pa-12 fill-height>
    <v-layout align-center text-center wrap>
      <v-flex xs12>
        <v-alert v-model="alert" type="error" dismissible>{{errorMessage}}</v-alert>
        <v-alert v-model="success" type="success" dismissible>SUCCESS!</v-alert>
        <v-img :src="require('../assets/thai monk.svg')" class="my-3" contain height="200"></v-img>
        <h1>「Devotter」</h1>
        <h2>
          その精進
          <br />ツイートしませんか
        </h2>
        <br />
        <h5>このアプリケーションは、1日1回AtCoderでのAC数をTwitterにツイートしてくれるアプリケーションです。</h5>
        <br />
        <v-form>
          <v-text-field
            @change="changeField"
            type="text"
            v-model="atcoderId"
            v-if="login"
            label="AtCoderID"
            color="#00acee"
            required
          ></v-text-field>
        </v-form>
        <v-btn
          @click="signin"
          v-bind:disabled="isnull"
          v-show="login"
          color="#00acee"
          rounded
        >Sign In To Twitter</v-btn>
        <v-btn @click="logout" v-show="!login" color="#00acee" rounded>Log Out from Twitter</v-btn>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import firebase from "firebase";
import axios from "axios";

export default {
  name: "guestUserScreen",
  methods: {
    logout: function() {
      firebase
        .auth()
        .signOut()
        .then(()=>{
          this.success=true;
          this.login=true;
        })
        .catch(()=>{
          this.alert=true;
          this.errorMessage="サインアウトに失敗しました。"
        });
    },
    signin: function() {
      var db = firebase.firestore();
      var document = this.atcoderId;
      axios
        .get(this.userApi + this.atcoderId)
        .then(response => {
          this.userInfo = response;
          const provider = new firebase.auth.TwitterAuthProvider();
          this.success = true;
          firebase
            .auth()
            .signInWithPopup(provider)
            .then(function(result) {
              let token = result.credential.accessToken;
              let secret = result.credential.secret;
              db.collection("users")
                .doc(document)
                .set({
                  accessToken: token,
                  accessTokenSecret: secret
                });
            })
            .catch(function() {
              this.alert = true;
              this.errorMessage = "ERROR!";
            });
        })
        .catch(() => {
          this.alert = true;
          this.errorMessage = "無効なAtCoderID名です。";
        });
    },
    changeField: function() {
      this.atcoderId ? (this.isnull = false) : (this.isnull = true);
    }
  },
  created: function() {
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        this.login = false;
      }
    });
  },
  data: function() {
    return {
      atcoderId: "",
      isnull: true,
      userInfo: "",
      userApi: "https://kenkoooo.com/atcoder/atcoder-api/v2/user_info?user=",
      alert: false,
      errorMessage: "",
      success: false,
      login: true
    };
  }
};
</script>

ソースコード(バックエンド)

//import
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const serviceAccount = require('./atcontributter-firebase-adminsdk-5p86i-6e55085ef8.json');
const axios = require('axios');
const twitter = require('twitter');
const cors = require('cors')({ origin: true });
const decycle = require('json-decycle').decycle;
const retrocycle = require('json-decycle').retrocycle;

//initialize
const adminConfig = JSON.parse(process.env.FIREBASE_CONFIG);
adminConfig.credential = admin.credential.cert(serviceAccount);
admin.initializeApp(adminConfig);

//create instance
const firestore = admin.firestore();
const bucket = admin.storage().bucket();

//global
let consumerKey;
let consumerKeySecret;
let accessToken;
let accessTokenSecret;
let receiveAc;
let receiveNewAcString;
let receiveNewAc;
let senderAc;
let uploadPath;
let problemCount;
let newProblemCount;

//main
exports.devotterCronJob = functions.region('asia-northeast1').https.onRequest(async (request, response) => {
    cors(request, response, () => {
        response.set('Access-Control-Allow-Origin', 'http://localhost:5000');
        response.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST');
        response.set('Access-Control-Allow-Headers', 'Content-Type');
    });
    try {
        //firebase storageから昨日のAtCoderAC情報が入ったjsonを取得する
        const receiveJson = await bucket.file('ac.json').download();
        receiveAc = JSON.parse(receiveJson);

        //kenkooooさんのAtCoderAPIを使って本日のAtCoderAC情報をaxiosで取得
        const receiveNewJson = await axios.get('https://kenkoooo.com/atcoder/resources/ac.json', { headers: { 'accept-encoding': 'gzip' } });
        receiveNewAcString = JSON.stringify(receiveNewJson, decycle());
        receiveNewAc = JSON.parse(receiveNewAcString, retrocycle()).data;

        //firestoreからtwitter apiのconsumerKeyとconsumerKeySecretを取得
        const getConsumerKeyQuerySnapShot = await firestore.collection('api').doc('keys').get();
        consumerKey = getConsumerKeyQuerySnapShot.data().consumerKey;
        consumerKeySecret = getConsumerKeyQuerySnapShot.data().consumerKeySecret;

        //firestoreからツイートに必要なユーザー情報を取得
        const getUserDataQuerySnapShot = await firestore.collection('users').get();
        getUserDataQuerySnapShot.forEach(async document => {
            try {
                //取得したUserQuerySnapShotからaccessTokenとaccessTokenSecretを取得
                const getAcceseTokenQuerySnapShot = await firestore.collection('users').doc(document.id).get();
                accessToken = getAcceseTokenQuerySnapShot.data().accessToken;
                accessTokenSecret = getAcceseTokenQuerySnapShot.data().accessTokenSecret;

                //昨日のjsonデータ中のfirestoreに登録しているユーザーのAC数を抽出
                for (const element in receiveAc) {
                    if (receiveAc[element].user_id === document.id) {
                        problemCount = receiveAc[element].problem_count;
                        break;
                    }
                }

                //今日のjsonデータ中のfirestoreに登録しているユーザーのAC数を抽出
                for (const element in receiveNewAc) {
                    if (receiveNewAc[element].user_id === document.id) {
                        newProblemCount = receiveNewAc[element].problem_count;
                        break;
                    }
                }

                //twitterクライアントにkeyを登録
                const client = new twitter({
                    consumer_key: consumerKey,
                    consumer_secret: consumerKeySecret,
                    access_token_key: accessToken,
                    access_token_secret: accessTokenSecret
                });

                //twitter apiを使ってツイート
                let tweetMessage = '今日の' + document.id + 'さんのAC数は' + (newProblemCount - problemCount) + 'でした。\n#devotter'
                client.post('statuses/update', {
                    status: tweetMessage
                }, (error) => {
                    if (!error) {
                        response.status(200).send('success!');
                    }
                    else {
                        response.status(500).send(error);
                    }
                });

                //今日取得したAC数をfirebase storageにアップロード
                senderAc = JSON.stringify(receiveNewAc);
                uploadPath = 'ac.json';
                bucket.file(uploadPath).save(senderAc);
            }
            catch (error) {
                response.status(500).send(error);
            }
        });
    }
    catch (error) {
        response.status(500).send(error);
    }
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

toioとTwitterAPIを合わせて使ってみた!

toio.jsとTwitter APIを合わせて使ってみた

久しぶりの記事投稿。今回はtoioをJavascriptで制御するのとTwitterAPIを合わせて使っていく。

toioとは

toioとはソニーインタラクティブエンタテインメントより発売されているロボットトイである。詳しくは公式サイトを見てほしいが、子供から大人まで楽しめるモノになっている。公式からtoio.jsというNode.js(サーバサイドで動くJavaScript環境)のライブラリが公開されているので今回をそちらを利用していく。

今回やること

toioの最大の特徴は主役であるデバイスのキューブ(下図)の光学センサーを用いて、付属のマットやシールに施されている特殊印刷を読み取り、キューブの絶対座標やIDを取得できる点だ。

今回はその絶対座標や各シールのIDを取得した後、TwitterAPIを介してそれらをTweetするのを目標とする。
なおJavascriptを触るのは初めてなのであしからず。

環境構築

環境構築は以下を行った。今回は手順は省く。
- Node.js及びtoio.jsの環境構築(公式を参照)
- TwitterAPIの導入(参照記事)

なおTwitterのアプリケーション作成方法はアップデート毎に変更されているようだ。3年前に一度Androidアプリ開発で触ったことがあったが、開発者アカウントの作成などは審査などが追加されており少々面倒くさくなっていた。

できたもの

今回はクラフトファイターの技カードのIDを識別してツイートする内容とした。(マットを使った座標が今後考えていく・・・)
以下が機能である。
- キューブはキーボートの矢印キーで走行可能
- プログラム実行中は常にIDを取得し続ける
- スペースキーを押すと現在のIDを利用してツイート文を判定・投稿

以下はオートタックルのカードを認識している。

スペースキーを押すと何やら詳細情報がたくさん吐き出されてTwitterに投稿される。
1.png

以下が投稿結果

2.png
ちゃんと投稿できてる。

投稿が完了するとプログラムは終了するようにした。
toioコンテンツ内の特殊印刷から得られるIDはこちらの技術情報から参照できる。

ちなみにもうひとつも(今度はタイフーン)
 
ツイート!
3.png
結果
4.png

ソースコード

const twitter = require("twitter");
const fs = require("fs");
const client = new twitter(JSON.parse(fs.readFileSync("secret.json", "utf-8")));
const keypress = require('keypress')
const { NearestScanner } = require('@toio/scanner')
const DURATION = 700 // ms
const SPEED = {
    forward: [70, 70],
    backward: [-70, -70],
    left: [30, 70],
    right: [70, 30],
}
const flag = true;

//tweet
function tweet(sentence) {
    client.post('statuses/update', { status:sentence }, function (error, tweet, response) {
        if (!error) {
            console.log(tweet);
            process.exit(0);
        } else {
            console.log('error');
            process.exit(1);
        }
    });
}

//make sentences from data
function make_sentence(value){
    if(value === 3670016){
        out = 'Typhoon!'
    }else{
        if(value === 3670054){
            out = 'Rush!'
        }else{
            if(value === 3670018){
                out = 'Auto Tackle!'
            }else{
                if(value === 3670056){
                    out = 'Random!'
                }else{
                    if(value === 3670020){
                        out = 'Stab Power Up!'
                    }else{
                        if(value === 3670058){
                            out = 'Slapping Power Up!'
                        }else{
                            if(value === 3670022){
                                out = 'Side Attack!'
                            }else{
                                if(value === 3670060){
                                    out ='Easy Mode!'
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

//exit processing
function exit(){
    try{
        if(flag){
            throw new Error('program end');
        }
        console.log('not end');
    }catch(e){
        console.log(e.message);
    }
}

//main
async function main() {
    // start a scanner to find nearest cube
    const cube = await new NearestScanner().start()

    // connect to the cube
    await cube.connect()

    //tweet data initialize
    var id = []

    //get id data
    cube
      .on('id:standard-id', data => {id.unshift(data.standardId)})
    //.on('id:position-id', data => {id.unshift(data.x,data.y)})  //appendix

    //move and tweet
    keypress(process.stdin)
    process.stdin.on('keypress', (ch, key) => {
        // ctrl+c or q -> exit process
        if ((key && key.ctrl && key.name === 'c') || (key && key.name === 'q')) {
            process.exit()
        }

        switch (key.name) {
            case 'space':
                var value = id[0];
                console.log('data : ', value)
                make_sentence(value)
                tweet(out)
                break
            case'':

                break
            case 'up':
                cube.move(...SPEED.forward, DURATION)                
                break
            case 'down':
                cube.move(...SPEED.backward, DURATION)
                break
            case 'left':
                cube.move(...SPEED.left, DURATION)
                break
            case 'right':
                cube.move(...SPEED.right, DURATION)
                break
        }
    })

    process.stdin.setRawMode(true)
    process.stdin.resume()

}

main()

いかにもJavascript初心者っぽいコーディングだがあしからず・・・
IDを判別して文章を決める大量のif else文をなんとかしたいが、何かいい方法はないだろうか・・・

今後

初挑戦のNode.jsもといJavaScriptだったがtoioの整った環境と使いやすいデバイス、仕様のおかげで1つ作成することができた。
今回は利用するID(toio内の利用物)を絞ったが、次回はマットから取得できるX,Y座標やアングル等を利用して発展させたいと考えている。

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

【Next.js/Nuxt.js】Nuxt.jsで言うfetch()やasyncData()のNext.js版→getInitialProps()

Next.jsのgetInitialProps()

Next.jsでも、Nuxt.jsで言うところのfetch()またはasyncData()にあたる処理を見つけました。
getInitialProps(context)です。

詳しくは、getInitialProps | 公式サイト(英語)に記載があります。
画面表示前にfetchができるとのことです。

※もし認識に誤りがある場合はコメント頂けるとありがたいです。

説明

getInitialProps()の引数は、Context Object | 公式サイト(英語)です。

jsxのthis.propsgetInitialProps()でreturnしたオブジェクトのプロパティが入ってきます。

以下は、上記の公式サイトより簡素に書き直したものです。

import React from 'react'

class Page extends React.Component {
  // 引数「context」については上述
  static async getInitialProps(context) {
    return { name: 'Hanako' }
  }

  render() {
    // this.propsにプロパティ「name」が入ってくる
    return <div>Name is {this.props.name}</div> 
  }
}

export default Page

参照URL

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

【Next.js/Nuxt.js】Nuxt.jsで言うasyncData()のNext.js版→getInitialProps()

Next.jsのgetInitialProps()

Next.jsでも、Nuxt.jsで言うところのasyncData()にあたる処理と思われる処理を見つけました。
getInitialProps(context)です。

詳しくは、getInitialProps | 公式サイト(英語)に記載があります。
画面表示前にfetchができるとのことです。

※もし認識に誤りがある場合はコメント頂けるとありがたいです。

説明

getInitialProps()の引数は、Context Object | 公式サイト(英語)です。

jsxのthis.propsgetInitialProps()でreturnしたオブジェクトのプロパティが入ってきます。

以下は、上記の公式サイトより簡素に書き直したものです。

import React from 'react'

class Page extends React.Component {
  // 引数「context」については上述
  static async getInitialProps(context) {
    return { name: 'Hanako' }
  }

  render() {
    // this.propsにプロパティ「name」が入ってくる
    return <div>Name is {this.props.name}</div> 
  }
}

export default Page

参照URL

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

Cloud Functions for Firebase で環境変数を扱うための方法

はじめに

Functionsにcrediancalな情報をもたせるにあたって、
可能な限り環境変数をそのまま利用した形式にしたかったのでその作業録となります。

外部APIのトークン情報などを環境変数で管理しており、CIや手元では環境変数の値で向き先を切り替えるといったことをしているので、可能な限りそこに合わせたかった都合もあります。

今回使用した言語はJavascriptで、webpack + Jestを利用している構成になります。

JavaScriptでWebpackを利用したFunctionsに関してはCloud Functions for Firebase入門 (簡単なテストまで)で記載したので、もしよければご確認ください。

公式が推奨する方法について

設定値をデプロイする方法

まず最初に公式で紹介されている設定値を与える方法を説明します。
これは環境変数をそのまま扱うわけではなく、予め設定値をデプロイしそれを利用する形式となります。
公式のリファレンスをみるとfirebaseコマンドで設定や取得もできます。コマンドで設定ファイルを登録後なら、SDKからも設定値の取得もできます。

  • 値の設定
$ firebase functions:config:set someservice.key="THE API KEY" someservice.id="THE CLIENT ID"
  • 値の取得(コマンドの場合)
$ firebase functions:config:get
# -> 結果
{
  "someservice": {
    "key":"THE API KEY",
    "id":"THE CLIENT ID"
  }
}

  • 値の取得(JavaScript SDKの場合)
const functions = require('firebase-functions');
const someservice = functions.config().someservice;

また、デフォルトでFunctionsのRuntime上のprocess.envに展開される環境変数もあるようなので、興味のある方は調べてみても面白いかもです。(参考: Firebase Cloud Functionsの環境変数はRuntimeによって変わるので注意 )

この方法について

公式が紹介している方法だけあって、こだわりがなく、複雑な設定項目が無いのならこれでもいいのかなと思います。

一方で自分は以下の観点で公式の方法は採用しない方針にしました。

  • 設定値の制限の厳しさ

    • キーにUpperCamelケースの値がなぜかだめだった。
    • いわゆる.envの内容を流用するスクリプトを書いていたら怒られました。
  • firebaseに依存する箇所の隠蔽化

    • 設定値がfirebase依存だと、全体的にfirebaseに依存する形になるので避けたかった。
    • テストを簡単にするため。
    • 将来的なfirebaseからの移行も検討の余地を残したかった。
  • 構成の一貫性

    • 環境の差異はほぼ.envで記述できる向き先が異なる程度にとどめたかった。
    • 環境の差異からくる事故や作業漏れのリスク軽減のため。

環境変数をそのまま渡す方法

色々方法が考えられると思いますが、簡単なものはbundle時に環境変数そのものを埋め込む方法かなと思います。それにはwebpackのval-loadlerが簡単に利用できてオススメです。

val-loaderを使った環境変数の渡し方

まずval-loaderをインストールします。

$ npm install val-loader --save-dev

次に環境変数(prcess.env)をbundleするためのファイル(env.js)を用意してwebpackにloaderの設定を追加すればOKです。

env.js
codeには実際にbundle時に評価されたい値を記載します。ほかにもキャッシュ用の項目などあったりするので、詳しくはval-loadlerを参照してください。

module.exports = (options, loaderContext) => {
    return { code: `module.exports = ${JSON.stringify(process.env)};` };
};

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /env.js$/,
        use: [
          {
            loader: `val-loader`,
          },
        ],
      },
    ],
  },
};

利用する側は下記のようにimportをすれば変数envはビルド時のprocess.envと同じように環境変数にアクセスすることができます。

// 環境変数に FOO=bar がセットされているとする

import env from "path/to/env.js";
// env["FOO"]
// -> bar となります。

jestでの対応について

ちなみにここまでの内容だけだとwebpackの機能を使うことになっているので、import部分がjestだと動かずに失敗してしまいます。
なので少し追加作業が必要です。本当はこの作業も割愛したいのですが、方法を知っている方いれば教えてください。

jest.config.js
jestでimportを動くするためにはtrasnform設定を追加してあげます。キーには変換するファイルパターン、変換先ファイルを指定します。
今回はimport env from "path/to/env.js";に対応する変換をjest用にしますので、それにマッチするパターンで記載します。これに限ったことではないですが、webpack側でloaderを追加した場合は何かしら対応するtransformを用意する必要があるみたいです。

module.exports = {
//…
    transform: {
        'env\\.js': '<rootDir>/tests/module/env.js',
         //…
    },
//…
};

tests/module/env.js
実質val-loader用ファイルと同じことをしています。webpackとjestでコンパイルに微妙に差があるっぽいので別のファイルが必要な感じです。

module.exports = {
    process(src, filename) {
        return `module.exports = ${JSON.stringify(process.env)}`;
    },
};

これでテストも無事に通るはずです。

おわりに

環境変数を可能な限りそのまま活用する方法について説明しました。
Functionsで環境変数のまま利用しようとしている記事が見当たらずって感じだったので、自分なりにまとめてみましたがどうだったしょうか?

credencialな設定値を環境変数で管理はどこもやっている気はするので、それをFunctionsで活用できたら結構便利なんじゃないでしょうか?おとなしくCloud Runを使う手も
同じような悩みを抱えている方の助けになれば幸いです。

感想や意見あればコメントあると嬉しいです。まさかりも歓迎です。

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

M5StickCのディスプレイに現在の天気を表示する。

はじめに

obnizOSを搭載したM5StickCのディスプレイに現在の天気を表示したいと思います。

HTMLとJavaScriptしか使わない簡単なコードなので、プログラミング初心者にも優しくなっています。

天気を取得するにはOpenWeatherMapという無料のAPIを使用します。

完成品

StickC_weather.jpg

用意するもの

作成手順

1. OpenWeatherMapにサインアップする

OpenWeatherMapにアクセスし、サインアップをしてください。

その後、サインアップ完了のメールが届きます。メールに記載してあるAPI Keyを控えておいてください。
API Keyは、Webサイトにログイン後、API Keysというページからも確認できます。

2. プログラムを書く

最終プログラムは以下になります。

<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
  <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>
  <script src="https://unpkg.com/obniz@3.2.0/obniz.js" crossorigin="anonymous"></script>
  <script src="https://unpkg.com/m5stickcjs/m5stickc.js"></script>
</head>
<body>
  <canvas id="canvas" width="80" height="160"></canvas>
  <script>
    const m5 = new M5StickC('OBNIZ_ID_HERE');
    m5.onconnect = async function () {
      console.log("connected");
      await m5.m5display.onWait();

      let ctx = document.getElementById("canvas").getContext("2d");
      let baseURL = "http://api.openweathermap.org";
      let area = "/data/2.5/weather?q=Tokyo,jp";
      let key = "&APPID=APPID_HERE";
      let url = baseURL + area + key;
      let weatherListEn = ["Clouds","Rain","Clear","Snow","Drizzle","Thunderstorm"];
      let weatherListJa = ["くもり","あめ","はれ","ゆき","きりさめ","かみなり"];

      $.ajax({
        url: url,
        type: 'GET',
        dataType: "json",
      })
      .done(function(response) {
        weather = response.weather[0].main;
        for(let i = 0; i < weatherListEn.length; i++){
          if(weather === weatherListEn[i]){
            weather = weatherListJa[i];
            break;
          }
        }
        console.log(weather);
        ctx.clearRect(0, 0, m5.m5display.width, m5.m5display.height);
        ctx.fillStyle = "#fff";
        ctx.font = "30px sans-serif";
        ctx.translate(m5.m5display.width - 30, m5.m5display.height - 10) ;  
        ctx.rotate(-90 * Math.PI / 180);
        ctx.fillText(weather, 0, 0);
        m5.m5display.draw(ctx);
      });
    };
  </script>
</body>
</html>


コードの解説

obniz.js, m5stickc.jsをそれぞれ読み込みます。
obniz.jsを読み込んだ後に、m5stickc.jsを読み込むようにしてください。

<script src="https://aframe.io/releases/latest/aframe.min.js"></script>
<script src="https://unpkg.com/obniz@3.2.0/obniz.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/m5stickcjs/m5stickc.js"></script>


次に、M5StickCのインスタンスを作成します。OBNIZ_ID_HEREにはあなたのobniz_idを入れてください。

const m5 = new M5StickC('OBNIZ_ID_HERE');


このコードではディスプレイの初期化処理をしています。

await m5.m5display.onWait();


次に、URLを指定します。baseURLはこのままで大丈夫です。
areaは「Tokyo,jp」の部分に調べたい都市の名前を入れてください。今回は東京の天気を取得します。
keyには「APPID_HERE」の部分に先ほど控えたOpenWeatherMapのAPI Keyを入力してください。

let baseURL = "http://api.openweathermap.org";
let area = "/data/2.5/weather?q=Tokyo,jp";
let key = "&APPID=APPID_HERE";


以下のコードでは、APIが返す天気の一覧とそれに対応する日本語を配列に格納しています。
本当はもう少し天気の種類は多いのですが、頻度が少ないので割愛しています。

let weatherListEn = ["Clouds","Rain","Clear","Snow","Drizzle","Thunderstorm"];
let weatherListJa = ["くもり","あめ","はれ","ゆき","きりさめ","かみなり"];


その後、AjaxでHTTP通信を行なっています。戻り値のresponse.weather[0].mainに天気が格納されているので変数weatherに代入します。
戻り値は英語なので日本語への変換を以下のコードで行います。

for(let i = 0; i < weatherListEn.length; i++){
  if(weather === weatherListEn[i]){
     weather = weatherListJa[i];
     break;
  }
}


今回はcanvasを利用して文字を表示します。canvasを使用することで文字を横向きに表示することが可能です。
ctx.translate()とctx.rotate()で文字を90度回転させています。
m5.m5display.draw()の引数にはcanvasオブジェクトのみ指定できます。

ctx.clearRect(0, 0, m5.m5display.width, m5.m5display.height);
ctx.fillStyle = "#fff";
ctx.font = "30px sans-serif";
ctx.translate(m5.m5display.width - 30, m5.m5display.height - 10) ;  
ctx.rotate(-90 * Math.PI / 180);
ctx.fillText(weather, 0, 0);
m5.m5display.draw(ctx);

3. M5StickCを電源に繋ぐ

電源が入ってもディスプレイには何も表示されません。起動したのかわかりづらいですが、電源ボタンを何度も押さないようにしましょう。

4. 実行

上手くいくと、このように現在の天気が表示されます。

StickC_weather.jpg

最後に

このコードにobnizのdeveloper's consoleからサーバレスイベントを使用すれば、毎日朝8時に天気を自動で取得して表示したり、30分毎に表示したりすることが可能です。

わざわざ、アプリやWebで天気を調べる必要もなくなるので是非作ってみてはいかがでしょうか。

最後まで読んでいただきありがとうございました。

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

【ニコニコ動画】他人のマイリストを自分のとりあえずマイリストに一括登録するブックマークレット

友人用に作成したけど、需要あるよと言われたので公開してみる。
他人のマイリスト一括で自分のとりあえずマイリストに登録するブックマックレットです。

コード

var token = document.getElementsByClassName('content_672')[0].getElementsByTagName('script')[1].innerText;
token = token.replace('FavMylist.csrf_token = "', '').replace('";', '').replace('\n','').replace(/\s+/g, "");
var watch = document.getElementsByClassName('watch');
var list = '';
for(var i = 0 ;  i < watch.length ; i++){
  var url = watch[i].href;
  var index = url.indexOf('sm');
  var id = url.slice(index);

  var request = new XMLHttpRequest();
  request.open('GET', 'https://www.nicovideo.jp/api/deflist/add?item_id=' + id + '&token=' + token, true);
  request.send();
}
window.alert('登録完了');

ブックマークレット作成

上記のコードをコンパイルしたもの

javascript:(function(){var c=document.getElementsByClassName("content_672")[0].getElementsByTagName("script")[1].innerText;c=c.replace('FavMylist.csrf_token = "',"").replace('";',"").replace("\n","").replace(/\s+/g,"");for(var e=document.getElementsByClassName("watch"),d=0;d<e.length;d++){var a=e[d].href,b=a.indexOf("sm");a=a.slice(b);b=new XMLHttpRequest;b.open("GET","https://www.nicovideo.jp/api/deflist/add?item_id="+a+"&token="+c,!0);b.send()}window.alert("\u767b\u9332\u5b8c\u4e86")})();

1.適当なサイト(ここでも良い)をブックマークする。
2.登録したブックマークを編集で開き、URLを削除して、上記のコンパイル済みコードを貼り付ける。
3.名前をわかりやすいものに変える。

使い方


こんな感じのマイリストページで先程作ったブックマークを開く(押す)。
そうすると、「とりあえずマイリスト」に「開いているページ内の動画」が登録されます。
なので、マイリストが100を超えていて複数ページある場合はページごとにブックマークレットを開く必要があります。

以上です。

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

plugin実践編:vue-scrolltoでスムーズにスクロール

vue-scrolltoとは

ページ内リンク(アンカーリンク)で
スクロールしながら指定場所に飛びます🧚‍♀️💫
【公式】
https://www.npmjs.com/package/vue-scrollto

比較: vue-scrollto未使用

味気ない🌀
nuxt-linkを使うと1回しか機能しない
a hrefでやれば解決するけど…
どうせならnuxt-link使いたいじゃない

anchor.gif

index.vue
<template>
 <div class="page">
   <nuxt-link to="#anchor">
     下に飛ぶ
   </nuxt-link>
   <div id="anchor">
     とべた!
   </div>
 </div>
</template>

比較: vue-scrollto使用

スクロールバーに着目👀
スルスルっとスクロール🍒
通常と違い、何回でもとべますね。
コードはstep順に記載していきます✍️

scrollto.gif

step1: インストール

ターミナル
$ npm i vue-scrollto
file
pages/
--| sample.vue

plugins/
--| vue-scrollto.js

nuxt.config.js

step2: /pluginsにjsファイルを追加

【vue-scrollto.js】
・Nuxt.jsでpluginを使用時の書き方
https://ja.nuxtjs.org/api/configuration-plugins/

・オプションは公式の
 Options項目で確認できます。

 duration: スクロール継続時間
 easing: 速度の緩急
 offset: 遷移後の位置調整
     少しだけ上に設定すると⭕️

easingに関してはここが参考になります!
https://note.com/ritar/n/n5e8ed0e07917

vue-scrollto.js
import Vue from 'vue'
import VueScrollTo from 'vue-scrollto'

Vue.use(VueScrollTo, {
 duration: 700,
 easing: [0, 0, 0.1, 1],
 offset: -100,
})

step3: nuxt.config.jsのpluginsに記載

nuxt.config.js
plugins: [
  '~plugins/vue-scrollto'
],

vue-scrollto.jsに記載したoptionsは
modulesに記載することもできます。
vue-scrollto公式のNuxt.js項目で確認できます。

ただvue-scrollto.jsに書いた方が
まとまって分かりやすいと思います💡

nuxt.config.js
 modules: [
   ['vue-scrollto/nuxt', { duration: 700 }],
 ],

step4: テンプレートでページ内リンクを作成

【sample.vue】
・nuxt-link toの中にv-scroll-toを入れる
 toが2個あって変な感じがしますが
 どちらも必要なので削らないように✏️
・リンク先を''で囲む

この書き方は
vue-scrollto公式の
Usage項目で確認できます。

sample.vue
<template>
 <div class="page">
   <nuxt-link
     v-scroll-to="'#anchor'"
     to
   >
     下にとぶ
   </nuxt-link>
   <div id="anchor">
     とべた!
   </div>
 </div>
</template>

これで完成です🤗🎉

ローカルver

一応ローカルの書き方も。
vue-scrollto.jsをまるごとコピペで⭕️
オプションもdata内に書けば適応されます。

sample.vue
<template>
 <div class="page">
   <nuxt-link
     v-scroll-to="'#anchor'"
     to
   >
     下にとぶ
   </nuxt-link>
   <div id="anchor">
     とべた!
   </div>
 </div>
</template>

<script>
import Vue from 'vue'
import VueScrollTo from 'vue-scrollto'

Vue.use(VueScrollTo, {
 duration: 700,
 easing: [0, 0, 0.1, 1],
 offset: -100,
})

export default {
 data () {
   return {
     options: {
       el: '#anchor',
       onDone: (el) => console.log(el)
     }
   }
 }
}
</script>

<style lang="scss" scoped>
 #anchor {
   margin-top: 1000px;
 }
</style>

今まで週3で投稿していましたが
来週から月水の2回に変更致します💡
自社サービスに手をつけているためです。
お楽しみに🌟

落ち着いたらまた週3になるかも?

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

Javascriptコーディングでグローバル汚染を防ぐために即時関数を利用する方法

Javascriptを用いた開発を行うときに、一般的に出回っている入門書のように、jsファイル内でそのまま変数を定義すると、他のjsファイルを読み込んだ際に、変数がバッティングしてコンテンツの機能が停止することがあります。
こういったことはバグの元ですし、特にチーム開発では大きな支障があります。

それを防ぐために、一つのjsファイル内で作成した変数が他コンテンツに干渉しないようにする必要があります。

具体的には、jsファイルを即時関数で囲うことになります。
以下に2通りのひな型を記します。

(function (root) {


  var initialize = 以下略
  var insert = 以下略

  root.SomeThing = root.SomeThing || {};

  // 他のjsファイルで用いたい関数を以下に記述
  root.SomeThing.Main = { 
    initialize: initialize,
    insert: insert
  };

}(typeof window === 'object' ? window : typeof global === 'object' ? global : this));

多数の関数や定数を流用したい場合は以下のやりかたもあります。

(function(root) {

  var SomethingConst = {};  //←ナニモノか連想配列を定義します(ここでは例としてSomethingConstとする)

  /*ここに、定義したい定数を用意します。定数は「SomethingConst.honyarara =」と言う形で記述*/

  root.SomethingConst = SomethingConst; //←rootのメソッドとして実行したい関数を設定

}(typeof window === 'object' ? window : typeof global === 'object' ? global : this));
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマホでIoTしてみた

スマホでIoTしてみた

概要

 Obnizのクラウドサービスを使って、LEDを点灯、モーターの制御をする。
 ミニ四駆を動かしたり、ラジコンをドリフトさせる。

Obnizとは?

obniz(オブナイズ)は、ON/OFFやUART通信などのIO制御をクラウドのAPI経由で行える仕組み。

(引用元: https://obniz.io/ja/how_obniz_works)

ラズベリーパイとどう違うの?

・JavaScriptで電子工作が可能に、面倒な初期設定は不要
・コンセプトは「ハードウェアのAPI化」

(引用元: https://jp.techcrunch.com/2018/11/22/cambrianrobotics-fundraising/)

手順

その1.Obnizをネットワークに繋げる

Obnizに電源を繋げる
画面に「WiFi Scanning..」が表示され、
接続先の一覧が表示されます。
mojikyo45_640-2.gif

左上の歯車みなものを操作して、接続先の選択とパスワードの入力を行い、
画面にQRコードが表示されればえば接続完了!
mojikyo45_640-2.gif

その2.LEDを発光させる

Pinを挿す位置に注意しながらObnizとLEDを繋げます。
QRコードをスマホで読み込むとエディターが表示されて、
クラウド上で実装することができます。
※QRコード横に表示されているIDを使って、PCからもエディターを開くこともできます。
mojikyo45_640-2.gif

その3.モーターを動かす

使おうと思ったラジコンキット備え付けのモーターだと遅すぎたので、
ミニ四駆のハイパーダッシュモーターとプラズマダッシュモーターを購入。おまけでアバンテも購入。
が、まさかのハイパーダッシュとプラズマダッシュ動かず・・・
アバンテ付属のモーターは動いたので少しもったいないお買い物になった。

実装

<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>
    <script src="https://unpkg.com/obniz@2.2.0/obniz.js" crossorigin="anonymous"></script>
  </head>
  <body>
    <div id="obniz-debug"></div>
    <input type="button" id="brake" value="ブレーキ">
    <input type="button" id="accel" value="アクセル">
    <br />
    <input type="range" id="inputPower" value="0" min="0" max="100" step="10">
    速度:<span id="textPower">0</span>
    <br />
    <input type="radio" name="inputFB" id="forward" checked><label for="forward">前進</label>
    <input type="radio" name="inputFB" id="back"><label for="back">後退</label>

    <script>

      $('#inputPower').change(function() {  
        $('#textPower').text($('#inputPower').val());
      });

      var obniz = new Obniz("ほげほげ");

      obniz.onconnect = async function () {
        var motor1 = obniz.wired("DCMotor", {forward:0 , back:1 });
        var motor2 = obniz.wired("DCMotor", {forward:2 , back:3 });
        var motor3 = obniz.wired("DCMotor", {forward:4 , back:5 });
        var motor4 = obniz.wired("LED", { anode:6, cathode:7 } );

        $('#accel').click(function () {
          motor1.power($('#inputPower').val());
          motor2.power($('#inputPower').val());
          motor3.power($('#inputPower').val());
          var booleanFB = $('input[name="inputFB"]:checked').attr('id') == 'forward' ? true : false;
          motor1.move(booleanFB);
          motor2.move(booleanFB);
          motor3.move(booleanFB);
          motor4.on();
        });

        $('#brake').click(function() {
          motor1.stop();
          motor2.stop();
          motor3.stop();
          motor4.off();
        });
      };
    </script>
  </body>
</html><!--<html>

その4.ラジコンを組み立てる

その3のときに買ったアバンテを組み立て。
組み立て後!
mojikyo45_640-2.gif

アバンテを組み立てましたが、
スマートカーをドリフトさせることになりました。

その5.ドリフト!

mojikyo45_640-2.gif
アバンテ付属のモーターを4つ集めるのは厳しいので結局ラジコンキット付属のモーターで動かすことにしました。
ドリフトしようとしてるのに4WDでステアリング不可とまったくドリフトに向いていない車体でやることに・・・。
それぞれのタイヤの回転速度を調整することでなんとかドリフトさせることができました。

実装 

<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>
    <script src="https://unpkg.com/obniz@2.2.0/obniz.js" crossorigin="anonymous"></script>
  </head>
  <body>
    <div id="obniz-debug"></div>
    <input type="button" id="brake" value="ブレーキ">
    <input type="button" id="accel" value="アクセル">
    <br />
    <input type="range" id="inputPower1" value="0" min="0" max="100" step="10">
    速度:<span id="textPower1">0</span>
    <br />
    <input type="range" id="inputPower2" value="0" min="0" max="100" step="10">
    速度:<span id="textPower2">0</span>
    <br />
    <input type="radio" name="inputFB" id="forward" checked><label for="forward">前進</label>
    <input type="radio" name="inputFB" id="back"><label for="back">後退</label>
    <br />
    <input type="button" id="right" value="右">
    <input type="button" id="left" value="左">
    <br />

    <script>

      $('#inputPower1').change(function() {  
        $('#textPower1').text($('#inputPower1').val());
      });

      $('#inputPower2').change(function() {  
        $('#textPower2').text($('#inputPower2').val());
      });

      var obniz = new Obniz("ほげほげ");

      obniz.onconnect = async function () {
        var motor1 = obniz.wired("DCMotor", {forward:0 , back:1 });
        var motor2 = obniz.wired("DCMotor", {forward:2 , back:3 });
        var motor3 = obniz.wired("LED", { anode:4, cathode:5 } );
        var motor4 = obniz.wired("LED", { anode:6, cathode:7 } );

        $('#right').click(function () {
          motor1.power(30);
          motor2.power(100);
          motor1.move(true);
          motor2.move(true);
          motor3.on();
          motor4.on();
        });

        $('#left').click(function () {
          motor1.power(100);
          motor2.power(30);
          motor1.move(true);
          motor2.move(true);
          motor3.on();
          motor4.on();
        });


        $('#accel').click(function () {
          motor1.power($('#inputPower1').val());
          motor2.power($('#inputPower2').val());
          var booleanFB = $('input[name="inputFB"]:checked').attr('id') == 'forward' ? true : false;
          motor1.move(booleanFB);
          motor2.move(booleanFB);
          motor3.on();
          motor4.on();
        });


        $('#brake').click(function() {
          motor1.stop();
          motor2.stop();
          motor3.off();
          motor4.off();
        });
      };

    </script>
  </body>
</html>

TIPS

  • WiFiが繋がらないことがあり、ファームフェアを更新することで繋がるようになった。
  • モーターの制御に必要なチャネルがモーター3つ分しかなかった(モーターとして制御できるのは3つまで)が、2つはLEDとして接続することで解消
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのclass構文を使わないクラスと継承

現在ではどのブラウザや環境でも、とりあえず提供されてる最新の安定バージョン使っておけばclass構文や、それにより提供されるconstructor、extendsによるサブクラスの作成(継承)が実現できます。

しかしJSの上記構文はあくまでシンタックスシュガーとなっており、裏ではprototypeにより実現されています。

私自身の考えですが、裏がどう動いているかを理解することで、表をより深く理解できると考えていますので、本記事はES2015以降見かけなくなったprototypeにを用いたクラスと継承を考えます。

今現在この記事を参考にprototypeを用いたコードを書くのはオススメしません。

以前書いていたようなコードだとしても・class構文がシンタックスシュガーだとしても、prototypeを汚染しかねないコードなのであくまで参考程度に


前提として、現在よく使われるJavaScriptのclass構文と継承は以下のようになります。

es2015
//getter,setterの都合上プロパティにはアンダースコアをつけています。
//#をつけてプライベートプロパティにすることも可能です

class Position {
  constructor(x, y) {
    this._x = x;
    this._y = y;
  }

  get position() {return {x:this._x, y:this._y};}
  get x() {return this._x;}
  get y() {return this._y;}

  set position(x, y) = {
    this._x = x;
    this._y = y;
  }
  set x(x) {this._x = x;}
  set y(y) {this._y = y;}
}

class Actor {
  constructor(name, life) {
    this._name = name;
    this._life = life;
  }

  get name() {return this._name;}
  get life() {return this._life;}

  isAlive() {return (this._life > 0);}

  damage(power) {this._life -= power;}

  dead() {this._life = 0;}
}

class Weapon extends Actor {
  constructor(name, life, power) {
    super(name, life);
    this._power = power;
  }

  get power() {return this._power;}

  use() {this.damage(1);}
}

class Character extends Actor {
  constructor(name, life, weapon, position) {
    super(name, life);
    this._weapon = weapon;
    this._position = position;
  }

  get weapon() {return this._weapon;}
  get position() {return this._position.position;}

  attack() {return this._weapon.power;}
}

class Enemy extends Character {
  constructor(name, life, weapon, position) {
    super(name, life, weapon, position);
  }
}

class Hero extends Character {
  constructor(name, life, weapon, position, level) {
    super(name, life, weapon, position);
    this._level = level;
  }

  get level() {return this._level;}

  attack() {return this._weapon.power + (this._level * 10);}

  levelUp() {this._level++;}
}

Positionクラスと、Actorクラスがあり、Actorを継承するWeaponクラスとCharacterクラスがあります。さらにCharacterクラスを継承するEnemyクラスとHeroクラスが定義されています。

Actorを継承している全クラスは名前とライフを持っており、キャラクターはダメージを食らうと・武器は使用するとライフが減っていきます。

また、ヒーローのみレベルがあり、任意のタイミングでレベルアップできます。ゲームであれば通常経験値を元にレベルアップしますが、レベル毎に必須経験値が変わる処理とか色々書くの面倒くさいし本記事の内容に関わってこないので除外します。

ES2015以降であれば、このようなコードで記述できます。

裏ではprototypeにより実現されています。では上記コードをprototypeらしく書き直してみましょう。

もちろん,es2015以前は無かったのでletもconst使いません。

prototype
var Position = function(x, y) {
  this._x = x;
  this._y = y;
};
Position.prototype = {
  getPosition: function() {return {x:this._x, y:this._y};},
  getX: function() {return this._x;},
  getY: function() {return this._y;},
  setPosition: function(x, y) {
    this._x = x;
    this._y = y;
  },
  setX: function(x) {this._x = x;},
  setY: function(y) {this._y = y;},
};

var Actor = function(name, life) {
  this._name = name;
  this._life = life;
};
Actor.prototype = {
  getName: function() {return this._name;},
  getLife: function() {return this._life;},
  isAlice: function() {return (this._life > 0);},
  damage: function(power) {this._life -= power;},
  dead: function() {this._life = 0;},
};

var Weapon = function(name, life, power) {
  Actor.call(this, name, life);
  this._power = power;
};
Object.setPrototypeOf(Weapon.prototype, Actor.prototype);
Weapon.prototype = {
  getPower: function() {return this._power;},
  use: function() {this.damage(1);},
};

var Character = function(name, life, weapon, position) {
  Actor.call(this, name, life);
  this._weapon = weapon;
  this._position = position;
};
Object.setPrototypeOf(Character.prototype, Actor.prototype);
Character.prototype = {
  getWeapon: function() {return this._weapon;},
  getPosition: function() {return this._position.getPosition();},
  attack: function() {return this._weapon.power();},
};

var Enemy = function(name, life, weapon, position) {
  Character.call(this, name, life, weapon, position);
};
Object.setPrototypeOf(Enemy.prototype, Character.prototype);

var Hero = function(name, life, weapon, position, level) {
  Character.call(this, name, life, weapon, position);
  this._level = level;
};
Object.setPrototypeOf(Hero.prototype, Character.prototype);
Hero.prototype = {
  getLevel: function() {return this._level;},
  attack: function() {return this._weapon.power() + (this._level * 10);},
  levelUp: function() {this._level++;},
};

このようになります。

prototypeでは継承をSUPER_CLASS.call(this, arg1, arg2, ...);と、Object.setPrototypeOf(SUB_CLASS.prototype, SUPER_CLASS.prototype);の2行で行えます。
callメソッドはclass構文のconstructorで呼び出すsuper()と同じようなものですが、setPrototypeOfメソッドは見慣れないものだと思います。

名前から推測できる通り、arg1のprototypeの中身をarg2のprototypeでセットします。
要はこういう事です。
arg1.prototype = arg2.prototype;

上の一行は簡単に書きすぎましたので、もう少し厳密に書くと以下のコードになります。こちらはMDNのsetPrototypeOfのポリフィルから引用しています。
引用元

setPrototypeOf
if (!Object.setPrototypeOf) {
     Object.prototype.setPrototypeOf = function(obj, proto) {
         if(obj.__proto__) {
             obj.__proto__ = proto;
             return obj;
         } else {
             var Fn = function() {
                 for (var key in obj) {
                     Object.defineProperty(this, key, {
                         value: obj[key],
                     });
                 }
             };
             Fn.prototype = proto;
             return new Fn();
         }
     }
}

setPrototypeOfが無かったらこうやって定義してね!というコードです。

このコードは、Object.create(null)のプロトタイプを返す場合が考慮されているので、あるクラスが他のクラスを継承したい、という要求を満たすコードは以下のように定義できます。

また、functionをクラスとして扱えていますが、要はFunctionなのでFunction.prototypeに継承する為のメソッドとして定義しています。

inherit
Function.prototype.inherit = function(superClass) {
  var tempClass = function() {};
  tempClass.prototype = superClass.prototype;
  this.prototype = new tempClass();
  this.prototype.constructor = this;
};

これにより、Object.setPrototypeOf(SUB_CLASS, SUPER_CLASS)SUB_CLASS.inherit(SUPER_CLASS);と書き直す事ができます。

JavaScriptはthisの扱い方が大変面倒くさいので、どこで書かれたthisが何を指しているのか、function式とarrow function式でのthisの束縛とか、bindとか、本当に分かりにくいですがthisとprototypeを理解することで、そこらへんが隠されたモダンなJavaScriptの流儀や思想もわかるようになるのではないかと思います。

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

初心者による ES6 引数

引数のデフォルト

ES6では引数のデフォルト値を宣言するには、「仮引数=デフォルト値」という形式で宣言します。

sample.js
function getPluss(a = 0,b = 0){
 return a + b
}

console.log(getPluss(1, 2))

デフォルト値を使う場合の注意点

1. デフォルト値が適用される場合、されない場合

デフォルト値が適用されるのは、引数が渡されなかった場合です。null, false, 0, 空文字列であっても、引数に渡されているのであればデフォルト値は適用されません。ただし、undefinedは大丈夫です。undefinedは「定義されていない = 渡されていない」だからです。

null / false / 0 / ' ' --> デフォルト値が発動しない
undefined --> デフォルト値が発動する

デフォルト値を持つ仮引数は、引数リストの末尾に置く

これは慣例として、デフォルト値を持つ引数の後方に持たない引数を記述するべきではない。

sample.js
//よい例
function getPluss(a = 0,b = 0){
 return a + b
}

//よい例
function getPluss(a ,b = 0){
 return a + b
}

//悪い例
function getPluss(a = 0,b ){
 return a + b
}

可変長引数の関数を定義する

ES6では、仮引数の前に「...(ピリオド3つ)」を書くことで可変長引数になります。可変長引数とは、渡された任意個の引数を配列としてまとめて受け取る機能です。

たとえば、

sample.js
//...numsで引数を配列として受け取る
function sum(...nums) {
 let result = 0
 for(let num of nums) {
  if(typeof num !== 'number'){
   throw new Error('これは数値ではありません: ' + num)
  }
 result += num
 } 
 return result
}

try {
 console.log(sum(1, 2, 3, 4, 5))
}catch(e){
 window.alert(e.massage)
}

この可変長引数を用いることのメリットは

1. 関数が可変長引数を取ることがわかりやすい

関数ブロック内でも、numsという可変長引数の名前でアクセスできるのでコードの可読性が上がる。

2. すべての配列操作が可能

可変長引数の配列は、Arrayオブジェクトであるためこれによる配列操作は可能であるため扱いやすい。

「...」演算子による引数の展開

「...」演算子は、実引数で利用することで、配列を個々の値に分解することができます。

sample.js
console.log(Math.max(1, 2, 3, 4, 5)) -> 5

console.log(Math.max([1, 2, 3, 4, 5])) -> NaN(エラー)

console.log(Math.max(...[1, 2, 3, 4, 5])) -> 5

実引数の前方に(...)を添えることで、きちんとmaxメソッドが使えるようになる。

名前付き引数でコードを読みやすくする

ES6では、分割代入を利用することで名前付きでよりシンプルに可読性高く表現できる。

sample.js
function getPluss({ a = 1, b = 1}){
 return a + b
}

console.log(getPluss({a : 2, b : 2}))

ここで重要なことは、仮引数を

{プロパティ名 = デフォルト値, ...}

で宣言することで、オブジェクトとして渡された引数を分解!して、関数の配下では個別の引数としてアクセスできることができる。

参考資料

山田祥寛様 「javascript本格入門」

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

同じテーブル内に複数の外部キーを設定する方法!!

はじめに

某プログラミングスクールで、担当した実装を復習していきたいと思います。
今回は、出品・取引中・売却済みのこの3つをクリックした際に、それぞれにあった商品を
表示させる実装を行いました。

これを実装するにあたって、1つのテーブル内に複数の外部キーを設定する必要があり、
ここで詰まったため、記録として残していきます。

ちなみにこんな感じの実装をしていきます。
c0b6f5dcc3487ef71b3605eb6e99ffe0.gif

工程

今回は、工程を以下に分けて説明をしていきます。
1.実装の大まかな説明とマイグレーションファイルの作成
2.モデルの作成
3.コントローラーの作成
4.hamlでの条件分岐設定
の順で行っていきます。
少し、長いですががんばっていきましょう。

解説

1.実装の大まかな説明とマイグレーションファイルの作成

はじめに出品中・取引中・売却済みを区別するために、
productというテーブル内に、seller_id・auction_id・buyer_idという
userと紐づく外部キーを3つ設定しました。

そして、出品中の際には、productのレコードの中から
seller_id(出品者)にだけ値が入っているレコードをDBから引っ張って来ています。

取引中の場合は、seller_id(出品者)とauction_id(取引者)がいる
productのレコードをDBから引っ張ってきています。

売却済みの場合は、seller_id(出品者)とbuyer_id(買取者)がいる
productのレコードをDBから引っ張って来ることで、
それぞれを区別してDBから取得してきています。

マイグレーションファイルはこんな感じです。
*今回の実装であれば、user側はテーブルを作成しidがあればOKだと思います。

products.rb(マイグレーションファイル)
class CreateProducts < ActiveRecord::Migration[5.2]
  def change
    create_table :products do |t|
      t.string :name, null: false
      t.references :seller, foreign_key: {to_table: :users} 
      t.references :buyer, foreign_key: {to_table: :users}
      t.references :auction, foreign_key: {to_table: :users}
      t.timestamps
    end
  end
end

「詰まったポイント その1」
(1)foreign_key: {to_table: :users}
通常であれば、

t.references :user, foreign_key: true

foreign_key: trueのみで外部キーを設定できるのですが、
今回のように、同じテーブル内に複数の外部キーを設定する場合、
foreign_key: trueで定義してしまうと、
カラム名がテーブル名_idになってしまうため、
複数カラムを設定したいときにうまくいかないことがありました。

そのため、{to_table: :テーブル名}で今回使用するテーブルを直接指定する必要があるようです。

「参考記事」
Railsで同じモデルを参照する外部キーを2つ以上もつ方法

2.モデルの作成

product.rb
class Product < ApplicationRecord
  belongs_to :seller, class_name: "User", optional: true,foreign_key: "seller_id"
  belongs_to :buyer, class_name: "User", optional: true,foreign_key: "buyer_id"
  belongs_to :auction, class_name: "User", optional: true,foreign_key: "auction_id"
end

各、外部キーをuserとアソシエーションを組んでいます。

user.rb
class User < ApplicationRecord
  has_many :saling_items, -> { where("seller_id is not NULL && buyer_id is NULL") }, class_name: "Product"
  has_many :sold_items, -> { where("seller_id is not NULL && buyer_id is not NULL && auction_id is NULL") }, class_name: "Product"
  has_many :auction_items, -> { where("seller_id is not NULL && auction_id is not NULL && buyer_id is NULL") }, class_name: "Product"
end

次に、user.rbに焦点を当てて説明をしていきます。

user.rb
has_many :saling_items, -> { where("saler_id is not NULL && buyer_id is NULL && auction_id is NULL") }, class_name: "Product"

この1文は、出品中のアイテムをproductのレコードから取得するための記述となっています。
今回でいう、出品中の商品とは言い換えると、

「seller_id(出品者)はいるが、まだ、buyer_id(買取者)またはauction_id(取引者)はいないproductのレコード」

を取得すればいいという形となるため、
上記のwhereの記述で制限することで、:saling_itemsカラムには
出品中の商品のみが取得できるという感じです。

user.rb
 has_many :auction_items, -> { where("seller_id is not NULL && auction_id is not NULL && buyer_id is NULL") }, class_name: "Product"

次に、取引中の商品の記述になります。
取引中は言い換えると

「seller_id(出品者)とauction_id(取引者)のユーザーが存在し、buyer_id(買取者)はまだ存在していないproductレコード」

という形となるため、
上記のwhereでの制限となっています。

user.rb
has_many :sold_items, -> { where("seller_id is not NULL && buyer_id is not NULL && auction_id is NULL") }, class_name: "Product"

最後に、売却済みの商品の記述です。
売却済みは言い換えると

「seller_id(出品者)とbuyer_id(買取者)は存在するが、auction_id(取引者)は存在していないproductレコード」

ということになるため、
上記のwhereでの制限となっています。

これで、とりあえずはproductとuser間のアソシエーションは終了です。

「参考記事」
【Rails】テーブル間の条件付きアソシエーションの設定【メルカリコピー作成記】

3.コントローラーの作成
1.2の記述で、マイグレーションファイルとアソシエーションを組んだため、
コントローラーでその取得したデーターを取り出す記述を行っていきます。

products.controller.rb(必要な箇所のみ記載)
class ProductsController < ApplicationController
  before_action :set_current_user_products,only:[:p_transaction,:p_exhibiting,:p_soldout]
  before_action :set_user,only:[:p_transaction,:p_exhibiting,:p_soldout]


  def p_exhibiting #出品中のアクション

  end

  def p_transaction  #取引中のアクション

  end

  def p_soldout    #売却済みのアクション

  end

  private

  def set_current_user_products
    if user_signed_in? 
      @products = current_user.products.includes(:seller,:buyer,:auction,:product_images)
    else
      redirect_to new_user_session_path
    end
  end

  def set_user
    @user = User.find(current_user.id)
  end
end

*product.conrollerで行っていますが、productとuserでネストをしている場合は、
user.controllerへ上記の記載をしても大丈夫だと思います。
*current_userを使用しているため、ログインしていない場合idがないため、
エラーが出てしまうことがあります。
その際は、DBへの直打ち等でユーザーを存在させる必要があると思います。(ここはあまり自信がないので、この方法でエラーをはいてしまったら、すみません。)

【解説】

@user = User.find(current_user.id)

この1行によって、まずはログインしているユーザーのレコードを
取得している形となっています。

    if user_signed_in? 
      @products = current_user.products.includes(:seller,:buyer,:auction,:product_images)
    else
      redirect_to new_user_session_path
    end

この記述によって、ログインしているユーザーが所持しているproductレコードのみを取得していきます。

「詰まったポイント その2」
(1)上記で、指定のuserやprodutのレコードの取得はできた。
だが、そもそもproductテーブルに複数のカラムを指定したが、どうやって・どのタイミングで狙ったidへいれるのかがわかりませんでした。

「解決策」

products.controller.rb
  def new
    @product = Product.new
    @product.product_images.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to root_path
    else
      redirect_to new_product_path,data: { turbolinks: false }
    end
  end

  private
  def product_params
    params.require(:product).permit(:name product_images_attributes: [:image, :_destroy]).merge(seller_id: current_user.id) #productやご自身のカラムに合わせて変更してください。
  end

まず、seller_idとは、出品者がもつidなため、出品する段階のnew・createの段階で、
そのユーザーが持っているidをseller_idへいれることによって解決しました。

@product.update(buyer_id: current_user.id)

また、buyer_idに関しては、上記の一行を購入する画面でいれることによって実装しました。

4.hamlでの条件分岐設定
最後に、コントローラーで取得してきた値を繰り返し処理する記述を加えていきます。

c0b6f5dcc3487ef71b3605eb6e99ffe0.gif

上記の画像のように、productに指定したレコードがある場合と、
ない場合で表示の仕方を変更する必要があるため、以下でif文による条件分岐を行っていきます。

p_exhibiting.html.haml(一部のみ表示しています。)
- if @user.saling_items.present?
  - @user.saling_items.each do |product|
    = link_to product_path(product),data: { turbolinks: false },class:"item_content" do
      .item_content__image
        = image_tag product.product_images[0].image.to_s,size:"58x48"
      .item_content__right
        .item_content__right--name
          =product.name
        .item_content__right__good
          .item_content__right__good--goods
            = icon("far","heart")
            0
          .item_content__right__good--comment
            = icon("far","comment-alt")
            0
          .item_content__right__good--exhibition
            出品中
      = icon('fas', 'angle-right', class: 'item_content__icon')
- else
  .pmain__bottom
    = image_tag "", class: "pmain__bottom--img", size: "100x100"
    .pmain__bottom--text
      出品中の商品がありません

*今回は、長いため出品中のみの記載としています。

特に重要な部分を記載していきます。

- if @user.saling_items.present?

この一行で、userのsaling_itemsがある場合は以下に記述した

- @user.saling_items.each do |product|

のsaling_itemsを繰り返すようにしています。

以上です。

最後に

長い行を読んでいただきありがとうございました。
所々、切り抜いて記事を書かせて頂いているため、間違っている箇所があった際には、
私の記述でエラーを起こしてしまい申し訳ありません。
また、間違っている箇所がありましたら、コメントをいただけると幸いです。
ご視聴ありがとうございました。

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

Strapiに認証機能を追加する(特定のユーザしかAPIが呼べない仕様に変更する)

Strapi APIに認証機能を追加してみる

前回の記事「Strapi (Headless CMS)を使ってみたので使い方まとめ + GraphQL」でHeadless CMS「Strapi」を使って簡易APIを作ってみました。
今回は特定のユーザしかAPIを呼び出せない様に、APIに認証を付けていきたいと思います。

ちなみに「公式サイト」に詳しく説明が載っているので、英語が読める方はこちらを読んで進めることを強くお勧めします。

この認証機能を使ったVueアプリのレポジトリーはこちらです
レポジトリ

事前知識

Strapiでは、「ロールと権限」というプラグインを使って認証を行います。(このプラグインは、デフォルトでインストールされいるはずです。)

このプラグインでは、「JSON Web Token (JWT)」を使い認証を行います。

JSON Web Tokenとは

JSON Web Token(JWT)とは、JSONというデータ構造で情報が表現されたフォーマットです。安全にデータを運ぶために使用されます。JWTの詳しい説明は、下記の記事がわかりやすいです。
JSON Web Token の効用
認証におけるJWTの利用について

ということで、StrapiはJWTを使って認証を行います。
APIリクエストが送信されると、サーバが「認証ヘッダーがあるか」と「ユーザからのリクエストは、リソースにアクセス可能なものか」を確認します。JWTにユーザIDが含まれているため、ユーザがどのグループに入っているか確かめることができます。結果としてリクエストされているルートにアクセスが可能かどうかの判断ができます。

ロールの種類

Strapiのロールには大きく分けて「public」と「authenticated」の2種類があります。

Public

「認証ヘッダ」がリクエストに含まれていない時に、このロールが使用されます。「全ての人」がこのロールに設定したエンドポイントにアクセス可能です。「find」や「findOne」などはこのpublicロールを使うといいと思います。

Authenticated

このロールでユーザがどのルートにアクセスできるかなど定義が可能です。ユーザを作成したときにデフォルトで付与されるのがこのロールです。

ユーザを作成する

Authenticatedのロールを持つユーザを作成してみます。

公式サイトにも書いてますが、基本的には下記のコードでユーザを作成できます。簡単!

import axios from 'axios';

axios
  .post('http://localhost:1337/auth/local/register', {
    username: 'Strapi user',
    email: 'user@strapi.io',
    password: 'strapiPassword',
  })
  .then(response => {
    // Handle success.
    console.log('Well done!');
    console.log('User profile', response.data.user);
    console.log('User token', response.data.jwt);
  })
  .catch(error => {
    // Handle error.
    console.log('An error occurred:', error);
  });

実装する

今回は私が練習用に作ったチェーン店メモアプリの「チェーン店めもったろう君」で上記の関数を実行してみます。(このアプリはVuetifyを使っているので、htmlタグはVeutify仕様になっています。)

「ユーザ登録」ボタンを押すと、usernameが「momoko1」のユーザがadminに登録されるはずです。
image.png

index.html
<v-row>
  <v-col cols="12" class="mt-4 d-flex justify-center">
    <v-btn 
    class="success"
    v-on:click="createUser"
    >
    ユーザ登録
    </v-btn>
  </v-col>
</v-row>
main.js
async createUser(){
  axios
  .post('http://localhost:1337/auth/local/register', {
    username: 'momoko1',
    email: 'momoko1@strapi.com',
    password: 'password',
  })
  .then(response => {
    console.log('Well done!');
    console.log('User profile', response.data.user);
    console.log('User token', response.data.jwt);
  })
  .catch(error => {
    console.log('An error occurred:', error);
  });
}

「ユーザ登録」ボタンを押すと、User profileとトークンが返ってきました。

image.png

strapiから「ロールと権限」→ Authenticatedにいくと・・・できてる!
image.png

このようにしてAuthenticatedのロールを持つユーザ作成ができます。

ログインしないとCRUD操作ができない様にする

それでは、ログインをしていないとチェーン店メモったろう君でチェーン店の登録も、データの取得もできない様にしていきます。

Authenticatedに権限を付与

まずは「ロールと権限」→ Publicから権限を全て外します。その次にAuthenticatedの権限で必要なCRUD操作にチェックを入れます。これでログインしていないユーザは何も操作ができない様になります。

image.png

image.png

実装する

index.html
<v-col>
  <v-form>
    <v-row
      justify="center"
    >
      <v-col
      cols="10"
      md="4"
      >
      <v-text-field
        v-model="email"
        label="Eメールアドレス"
      ></v-text-field>
      </v-col>
      <v-col
        cols="10"
        md="4"
      >
        <v-text-field
          v-model="password"
          label="パスワード"
        ></v-text-field>
      </v-col>
      <v-col
        md="1"
      >
        <v-btn 
          class="success"
          v-on:click="login"
          >
          ログイン
        </v-btn>
      </v-col>
    </v-row>
  </v-form>
</v-col>
main.js
async login() {
  axios
    .post('http://localhost:1337/auth/local', {
      identifier: this.email,
      password: this.password,
    })
    .then(response => {
      console.log('Well done!');
      console.log('User profile', response.data.user);
      console.log('User token', response.data.jwt);
    })
    .catch(error => {
      console.log('An error occurred:', error);
    });
}

先ほどのユーザ登録の際に使用したEメールアドレスとパスワードを使用してログインしてみます。ちなみに、identifierに入れるのはemailでもusernameでも良い様です。

Image from Gyazo

ログインが成功すると、こんな感じでUser Tokenがレスポンスとして返ってきます。
image.png

このトークンをリクエストヘッダに含めて認証を行い、認証が通ればCRUD操作ができる、という仕組みです。tokenという変数にresponse.data.jwtを格納する様にコードを修正します。

main.js
async login() {
  axios
  .post('https://powerful-dusk-72165.herokuapp.com/auth/local', {
    identifier: this.email,
    password: this.password,
  })
  .then(response => {
    this.token = response.data.jwt
    this.getRestaurants() // ログインが成功したらレストランデータをとる関数を動かす
  })
  .catch(error => {
    console.log('An error occurred:', error);
  });
},

あとは、

main.js
// 修正後
async getRestaurants(){
  try {
    var result = await axios({
      method: "POST",
      url: this.apiURL,
      headers: {
        Authorization: `Bearer ${this.token}`,
      },
      data: {
        query: `
          query getRestaurants {
            restaurants {
              id
              name
              description
            }
          }
        `
          }
      });
      this.restaurants = result.data.data.restaurants;
  } catch (error) {
    console.error(error);
  }
}   

// 修正前
async getRestaurants(){
  try {
    var result = await axios({
      method: "POST",
      url: this.apiURL,
      data: {
        query: `
          query getRestaurants {
            restaurants {
              id
              name
              description
            }
          }
        `
          }
      });
      this.restaurants = result.data.data.restaurants;
  } catch (error) {
    console.error(error);
  }
}    

できた!
Image from Gyazo

Strapiでユーザ認証をしてAPIを呼び出す方法でした。結構簡単でびっくり。SNSログインもできるみたいなので、また今度Qiita記事を書きながら試してみたいと思います。
公式サイトのドキュメントがとても丁寧なのもStrapiの魅力ですね。好きになっちゃいそう・・・。

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

モダンブラウザにおけるキー入力のキャンセル

モチベーション

Markdownエディタを作っていたが、Macの動作がWindowsやLinuxとは微妙に異なり、仕様変更が余儀なくされた。

一方で、Fire Foxは異なるOS間でも一貫性をもっており、素晴らしい。

ChromeはIME入力時のKeyDownイベントでevent.keyの値が非IME時と異なってくれれば制御しやすかった。、特に開始時には非IMEでの入力と区別ができないので困った。

新Edge(Chromium)は今回は未調査。おそらくChromeと同様だろう。

基本的な方針と解決策

Markdownに対応したリッチなユーザー操作をさせつつ、Undoの履歴を完全に管理するには次の二通りの方針が考えられる。

  1. ブラウザによるDOMの操作はさせない。ユーザーによるキー入力をpreventDefaultし、その上で、そのキーに対応したDOM操作を自前で全て実装し、自前のUndo履歴に登録する。
  2. ブラウザによるDOM操作を行わせた後、操作された痕跡を調査してその内容をUndo履歴に登録する。

ここで、今回は前者の方針を取った。後者の場合だと、DOM操作前の状態を常にモニタリングしておかない限り、操作の内容を完全に把握することが困難である。特にテキスト入力以外のDOM操作(文字の削除やタグのBRタグの入力・削除など)の把握は大変難しい。またMarkdownに対応したDOMツリー構造の制限を設けるとなると、ブラウザによるDOM操作とは異なる操作をすることになる。であれば、最初から全てのブラウザによるDOM操作をキャンセルしてしまい、全て自前で操作した方が把握できて良い。

前者に従うには、大まかに次のような処理を行う。

  • KeydownイベントにおいてpreventDefaultし、ブラウザによるDOM操作をキャンセルする。同時に自前でDOM操作を行う。

しかし、実際には次の問題と回避策をとる。

  • IME入力はpreventDefault()でキャンセルできない。その為、IME入力に関しては、後者の方針を取らざるを得ない。IME入力中(compositionstart以後)のKeydownでは自前DOM操作は行わないようにし、compositionendイベントにおいて、ブラウザが操作した内容を推理し、Undo履歴だけを残す。IME操作に関する詳細は以前の記事"モダンブラウザにおけるIME入力検知"を参照されたし。
  • Safari(Mac)ではKeydownにおけるpreventDefault()での非IME入力のキャンセルが無効である。おそらくbeforeinputとinputがkeydownよりも先に発火する為である。幸い、beforeinputでpreventDefault()することで文字入力をキャンセルできるので、これを利用し、keydownで行うpreventDefaultと自前DOM操作を、beforeinputで行う。
  • Chrome(Mac)では、IME入力開始(最初の一文字目の入力)のcompositionstartの直前にKeydown (event.key==アルファベット)が発火するため、非IME入力の場合と区別できない。幸い、beforeinputはcompositionstartの後に発火し、かつ、beforeinputでpreventDefault()することで文字入力をキャンセルできる。よって、Safari(Mac)と同様に、keydownで行うpreventDefaultと自前DOM操作を、beforeinputで行う。
  • beforeinputは旧EdgeとFire Foxで発火しない。全ブラウザで共通して発火するのはinputイベントのみ。さらに、旧Edgeではもinputイベントが発火するものの、event.data等は全てundefinedである。よって、旧EdgeとFireFoxのpreventDefaultと自前DOM操作はkeydownで行う。
  • 文字入力以外のキー入力イベント(Delete,backspace,Enter,Tabなど)は、どのブラウザでもkeydownでpreventDefault()することでキャンセルできる。あわせて自前DOM操作も行う。

これらの方針をとった理由として、以下に各ブラウザでの動作試験の様子を記載する。

Macにおける問題

  • SafariはkeydownでpreventDefaultが効かない。
  • ChromeはIME入力時にもkeydownイベント(event.keyがアルファベット)が発火。windows10では発火しないので、IME入力と、アルファベット入力で処理を分けられた。Macでは発火してしまうために、IME入力開始時の最初のkeydownにおいて、アルファベット入力なのかIMEなのか判断できない。

仕方ないので再調査。代わりにinputイベントを使えないか。

beforeinputは旧EdgeとFire Foxで発火しない。全ブラウザで共通して発火するのはinputイベントのみ。

inputイベントは次のプロパティーを持つ

  • event.data: 入力した文字列が格納されている。IME入力中では、updateの度にinputイベントが発火し、dataには変換途中の文字列が入っている。
  • event.event.inputType: アルファベット入力、IME入力、delete、backspaceなどが分類されて格納されている。しかし、ここでdelete、backspaceを拾った時には既に文字は消えており、元の文字がなんであったか取得できない。つまり独自Undoを実装するには、やはりkeydownやbeforeinputで拾うしかない。

ここでSafariが圧倒的に気持ち悪いのは、なぜかEnter入力時のイベント発火順序はkeydownからなのに、文字入力時の発火順序はkeydownが最後ということ。そしてEnter入力はkeydownでpreventDefaultが有効。やはり、Safariにおいて文字入力にkeydownでpreventDefaultが効かないのは発火順序の問題なのでは。

イベント発火順序

アルファベット1文字入力

旧Eege Fire Fox (Win&Mac&Linux) Chrome (Win&Mac&Linux) Safari(Mac)
keydown keydown keydown
beforeinput beforeinput
input(※1) input input input
keydown※2

※1:旧Edgeでのinputイベントはdata==undefined, inputType==undefinedとなって必要な情報は何も取れない。

※2:なぜかkeydownが後に来る。その為、keydownでpreventDefaultしても効果が無いのかもしれない。

Enter入力(非IMEモード)

旧Eege Fire Fox (Win&Mac&Linux) Chrome (Win&Mac&Linux) Safari(Mac)
keydown keydown keydown keydown
beforeinput (insertParagraph) beforeinput (insertParagraph)
input(※1) input (insertParagraph) input (insertParagraph) input (insertParagraph)

IME入力開始(最初の一文字, IME直前のConvertキー相当のkeydownイベントは除く。)

旧Eege Fire Fox (Win&Mac&Linux) Chrome (Win&Mac&Linux) Safari(Mac)
keydown (Unidentified)※3 keydown (Process)※3 keydown (Process: Win, AsciiChar:Mac, Unidentified:Linux)※4
compositionstart compositionstart compositionstart compositionstart
compositionupdate
beforeinput beforeinput
compositionupdate compositionupdate
input(※1) input input input
compositionupdate
keydown (AsciiChar)※4

※3:以後Keydown (hogehoge)と記載した場合は、event.key==="hogehoge"であることを指す。つまり、"Unidentified"や"Process"の場合には本当に押したキーは取得できない。

※4:一方で"AsciiChar"と記載した場合は実際に押したキー(アルファベット文字)がevent.keyで取れることを指す。

IME入力の変更(文字追加やスペースキーによる変換)

旧Eege Fire Fox (Win&Mac&Linux) Chrome (Win&Mac&Linux) Safari(Mac)
keydown (Unidentified) keydown (Process) keydown (Process: Win, AsciiChar:Mac, Unidentified:Linux)
compositionupdate
beforeinput beforeinput
compositionupdate compositionupdate
input(※1) input input input
compositionupdate
keydown (AsciiChar)

IME入力の終了(Enterによる決定)

旧Eege Fire Fox (Win&Mac&Linux) Chrome (Win&Mac&Linux) Safari(Mac)
keydown (Process) keydown (Process:Win, Enter:Mac, Unidentified:Linux)
beforeinput (deleteCompositionText) ※8
input (deleteCompositionText) ※8
beforeinput beforeinput (insertFromComposition) ※8
compositionupdate
input input input (insertFromComposition) ※8
compositionend compositionend compositionend compositionend
input(※5)
keydown (Unidentified, Linuxのみ)※6 keydown(Enter)※7

※5:event.isComposing==falseとなっているが、inputType==="insertCompositionText"となっているため、非IMEのアルファベット入力(inputType==="insertText")とは区別可能。

※6:Linuxの場合のみ、ここで二度目のKeydownが発生する。

※7:このkeydownがIME決定であるという情報を取得する術がない。単純に"IME入力後のcompositionendの直後のkeydown(Enter)"という条件にしてしまうと、マウスクリックでIMEを終了させた場合にこの条件がNGとなる。ありそうなのは"input (insertFromComposition)の直後の後のkeydown(Enter)"ということならIME入力決定のEnterとして判別可能かもしれない。

別の場所をマウスクリックしてカーソルを移動させることによりIME入力終了させた場合

旧Eege Fire Fox (Win&Mac&Linux) Chrome (Win&Mac&Linux) Safari(Mac)
compositionend (カーソル移動後のノード) compositionend (入力textノード) compositionend (入力textノード) compositionend (入力textノード)
input (入力textノード)

Enterキーによる決定と比べて、beforeinputやinputが発火しない。

カッコ内はそのイベントハンドラにおいてdocument.getSelection()で取得できるfocus位置。旧Edgeだけが問題を持っている。

その他の注意

addEventListennerで同じイベントに二つのハンドラを登録した場合、前者でstopPropagationを行ってもでは後者のイベントが発生する。防ぐ場合はstopImmediatePropagationを発行する。

//example
my_div.addEventListener("keydown", OnKeydown1, false);
my_div.addEventListener("keydown", OnKeydown2, false);
my_div.addEventListener("keydown", OnKeydown3, false);

function OnKeydown1(event){
    event.stopPropagation();      //cannot cancel next event
    event.preventDefault();
}

function OnKeydown2(event){
    //fired
    event.stopImmediatePropagation();  //cancel next event//
    event.preventDefault();
}

function OnKeydown3(event){
    //not fired
}

ブラウザによる勝手なDOM改変

また、ブラウザによる勝手なDOM操作として、preventDefaultやそのタイミングでは回避できないものとしての事例に遭遇した。

  • 半角スペースを入力すると、$nbsp;に置き換わる場合がある。しかし、テキストノードのメソッドであるinsertData()を利用して半角スペースを挿入した場合にも置き換わってしまう。この点は諦めて、markdown出力時に、$nbsp;と半角スペースの置換を行ってユーザーには認識させないくらいしか手がなさそう。
  • IME入力によるBRタグの生成や消滅。非IME入力と違ってprventDefault()できないので、compositionstart時にBRタグの存在を記録しておいて、compositionend時に削除されていたりインスタンスが異なっていれば、BRタグの消滅や再生成が行われたものとしてUndo履歴に記録する。

最後に

この記事がどこかのだれかの役に立つといいのですが。
ElectronやNWJSでラップした場合の動作はChromeと同様と盲目的に信じているけど、果たして、、、

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

初心者による ES6 export/import

概要

アプリの規模が大きくなるほど、アプリを機能ごとに分割/整理するモジュールの存在はますます重要になってくる。

基本的な書き方

sample.js
import { name, ...} from module
 //name : インポートする要素
 //module : モジュール(拡張子はつけない)

export class XXX { }

がexport/importの基本的な書き方である。外部からアクセスできる要素にのみexportキーワードをつける。たとえば、

sample1.js
const name = 'Satou Shio'

export class Member {
 //コード
}

export class Class {
 //コード
}
sample2.js
import { Member, Class } from './sample1'

let member = new Member('佐藤')
console.log(member.getName())

import命令の様々な記法

importには、様々な書き方がある。

1. モジュール全体をまとめてインポート

アスタリスク(*)でモジュール内のすべての要素をインポートできる。この場合、asキーワードでモジュール名を別途指定しておく必要があります。

sample.js
import * as app from './sample1'

let member = new app.Member('佐藤')
console.log(member.getName())

2. モジュール内の個々の要素に別々に名前をつける

「クラス名 as 新しい名前」 のようにしてimportする。

sample.js
import { Member as MyMember, Class as MyClass} from './sample1'

let member = new MyMember('佐藤')
console.log(member.getName())

3. デフォルトのエクスポートをインポートする

モジュールに含まれる要素が一つだけであれば、デフォルトのエクスポートを宣言することができる。デフォルトエクスポートでは、クラス/関数などの名前は不要です。

sample1.js
export default class {
 static getPluss(a, b){
  return a + b
 }
}
sample2.js
import Math from './sample1'

console.log(Math.getPluss(1, 2))

参考資料

山田祥寛様 「javascript本格入門」

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

ブロックリーゲームズ:タートル9 正解でない?

ブロックリーゲームズ:タートル9

暗い色は、「このように描画せよ」という問題

色は間違っていない。

正解判定にならず、次の問題へ移行しない。

ブロックリーゲームズ:タートル9_できているのになぜ?.gif

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

JSのプログラムでincludeするコンパイラ的な何かをかいたお話

どうも、フミです。
京都の上のほうから市内に向かう電車の中で書いているので、誤字はお許しください...

普段からC系の言語とJavascriptを二刀使いしている皆さん、皆さんはJavascriptをかいていて、あ~、include文使えたらいいのにな...
って思ったことありませんか?

いや、いやいや、シンキングフェイスをしたそこのあなた、あなたの気持ちはよ~くわかりますとも!ええ、ええ、そうでしょうね!importを使えばいいだろう!でしょうね!!

使いこなせるのであればいいのですが、importは少し書き方が特殊になる上に、スコープ的にめんどくさいんです(個人の感想です。)

そこで、JSでもinclude文が使えるようになるためのプログラムを作りました!!
5分くおりてぃなので悪しからず...

やったこと

Nodejsを使って既存のファイルを解析し、include文を適用したjsファイルをa.jsとしてカレントディレクトリに出力します。

多分コード読んだほうがわかるので...

cmp.js
const main_file=process.argv[2];
const fs = require('fs');
function include(fname){
    let code = String(fs.readFileSync(fname));
    console.log("include \n\n"+code);
    const analy=code.split("#include ");
    let program=analy[0];
    for(let i=1;i<analy.length;i++){
        program+="\n"+include(analy[i].split(/\r\n|\n|\r/)[0])+"\n";
    }
    program+=analy.pop().split(/\r\n|\n|\r/,2)[1];
    return program;
}
fs.writeFileSync("a.js", include(main_file));

以上!!

使い方

コンパイル()したいファイルとincludeしたいファイルの拡張子は何でもいいです。
コマンドラインで

node cmp.js [コンパイルしたいメインファイルのパス]

としてやるとa.jsに結果が出力されます。
includeしたいファイル内でさらにincludeしても大丈夫です。

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

初心者による 静的プロパティ/静的メソッド

静的プロパティ/メソッドとは

静的プロパティ/静的メソッドとは、「インスタンスを生成しなくてもオブジェクトから直接呼び出せるプロパティ/メソッド」のことである。これに対して、インスタンスを呼び出して、インスタンス経由で呼び出すプロパティ/メソッドのことをインスタンスプロパティ/インスタンスメソッドという。

sample.js
オブジェクト名.プロパティ名 = 
オブジェクト名.メソッド名 = function()

または

class Math {
 static getPluss(a, b){
  return a + b
 }
}

のように定義する。

静的プロパティ/メソッドを定義するときの注意点

静的プロパティは、基本的に読み取り専用の用途で使う。

静的プロパティはインスタンスごとの値ではなくクラスごとの値であるため、基本的に変更すべき値ではない。よって、静的プロパティは読み取り専用の用途で使う。

静的メソッドのなかで、thisキーワードは使えない

thisキーワードはインスタンス自身を表すため、クラス単位ごとのメソッドである静的メソッドではthisキーワードは使えない、というよりも使っても意味がない。

なぜ静的メンバーをつかうのか

静的プロパティ/静的メソッドは機能的にはグローバルプロパティ/グローバルメソッドとは何ら変わりません。ではなぜ静的プロパティ/静的メソッドを使うのかというと、「名前の競合」を回避するためです。
クラスごとのプロパティ/メソッドである静的プロパティ/静的メソッドを使えば、たくさんのライブラリを使う開発であっても機能ごとにグローバルオブジェクトのように使えるので、大変勝手が良い。
よって、グローバル変数/関数は出来るだけ減らし、静的プロパティ/静的メソッドに情報をまとめるのが好ましい。

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

【Nuxt.js】 asyncDataとfetchって結局どう使うの?

TL;DR.

  • asyncDatafetchはコンポーネントがロードされる度に呼び出され、外部からデータを取得する際に使用する
  • acyncDataは外部から取得したデータをページコンポーネントのみで使用する場合に使用する
  • fetchは取得したデータをVuexのstoreに格納して使用したい場合に使用する

そもそもacyncDataとfetchって何?

  • acyncDataとfetchはページコンポーネントの初期化前に呼び出される関数のこと。
  • SSR(サーバーサイドレンダリング)、ページ遷移前にも呼び出される。
  • 第1引数にcontext(オブジェクト)を取るので、クエリパラメータなどの値にアクセスして処理を行うことができる。
  • 一方で、コンポーネントのインスタンスが作成される前に処理が実行されてしまうため、thisでコンポーネントのインスタンスにアクセスすることができない。

aycncDataの使用法

acyncDataは外部からデータを取得し、ページコンポーネントへ直接セットすることを目的として使用される。asyncDataによって返される値はコンポーネントのテンプレートからアクセスすることで使用できる。

<template>
  <div>{{ hoge }}</div>
</template>

<script>
export default {
  async asyncData({ app }) {
    const doc = await app.$firebase.firestore().collection('hoge').get()
    return { hoge: doc.data().hoge }
  }
}
</script>

fetchの使用法

fetchは外部から取得したデータをVuexのstoreに格納して使用することを目的として使用される。acyncDataとは違ってコンポーネントに値を直接セットすることができない。

<template>
  <div>{{ $store.state.hige }}</div>
</template>

<script>
export default {
  async fetch({ app, store }) {
    const doc = await app.$firebase.firestore().collection('hige').get()
    store.commit('setHige', doc.data().hige)
  }
}
</script>

結局どう使うの?

  • SSRとCSRのライフサイクルはpluginmiddlewarevalidate→asyncData→fetchbeforeCreateの順で処理が実行されているので、タイミング的にはどちらを使用しても影響範囲は変わらないように思える。

  • asyncDataの場合、そのページで直接apiを叩いてデータを取得することを目的としているため、Vuexを使用して共通化している関数などを使用しない。したがって、storeに格納しないデータ(アカウントデータやテーマなどの情報はlocalStrageやsessionStrageかから取得することを想定)を使用してデータ取得を行いたい場合に使用するイメージ。また、asyncData内でstoreを使用することはできるが、gettersを動かしてページにデータを持ってくる必要がある。

  • fetchの場合、Vuexのmutationsを直接叩いてstoreに値をセットして、ページから直接storeを参照するために使用する。したがって、取得したデータの加工や表示方法をstoreに入れる前に全て行う必要がある。つまり、一旦storeに入れてしまったデータを編集して表示することが難しい。取得したデータを完全にページで成形して使用する場合(マスターデータや表示のみで使用する一覧系データなど)に使用するイメージ。また、アプリケーションないで使用しているデータや処理方法がVuexに大きく依存している場合、かなり有効になる。

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

Excelの資料作成で楽をするためにツールを作ってみた

概要

Excelの資料作成でちょっとでも楽がしたかったので、「Doci」というツールをつくりました。
本記事では、新たに技術を学ぶことではなく、培った知識・経験で日常の課題をいかに解決していくかにフォーカスしております。ですので、コードそのものよりも、「〜〜したいをどうコードに落とし込むか」を中心に書いています。

以下に簡単な動作デモを載せます。

doci.gif

動作環境

とにかく挫折しないことを目指して、自分が使いやすい形でつくったので、以下のようにゆるい感じになっています。

  • PCで使うことのみを想定しているので、レスポンシブ対応は考慮対象からはずしました
  • クリップボードにアクセスする機能はブラウザによって挙動がさまざまだったので、Chromeのみに対応する形としました

各種リンク

利用した技術

  • JavaScript(ES2015)
  • Sass

解決したかった課題

システムがインテグレーションする感じのお仕事をしていると、業務の10割大部分でExcelを使用し、資料やテスト証跡などをつくることになります。
Excelは表計算ソフトなので、資料をつくっていると、「これはExcelだとちょっと面倒だな...」と思うことが多々あります。

スクリーンショットを撮影したら、自動でExcelに貼り付けてくれる素敵ツールなどもあるようですが、資料やテスト証跡では、おもてなしの精神を以ってスクリーンショットの大事な部分を枠で囲ったり、吹き出しでコメントを入れる作業が別で必要になります。個人的にはこちらの方がスクショぺたぺたよりも、しんどい部分だと感じています。

これをいい感じに解決してくれるツールは見当たらなかったので、無いならつくれば良いじゃないの精神で、1ヶ月少々かけてざっくりとつくってみました。

以下では、課題の詳細等について記述していきます。

現状

アプリを作る上で、あれも欲しい、これも欲しいと最初から構想を膨らませていき、幾度となく散ってきたので、まずは、「これは外せない」と思う機能をリストアップします。箇条書きでボリュームを目に見えるよう整理することで、最低限実装すべきものに注力できるようにします。

絶対欲しい機能

  • マウスでカチカチ吹き出しやら枠やらを切り替えるのが面倒なので、さくっと切り替えられるキーボードショートカットが欲しい
  • ぺたぺた貼り付けた画像にコメントを入れていると、スクロールするのが手間だから、まとめて編集したい
  • アプリからExcelへ貼り付ける作業が面倒になったら元も子もないので、結果をすぐにExcelに貼り付けられるようにしたい

これらの「願望」から、実装に落とし込むため、「要件」として少し整理してみます。

願望は、いくらでも素敵に実現できる可能性を秘めていますが、思いのおもむくままに進めると、大体大きな落とし穴に叩き落とされてしまいます。これを防ぐため、既存のライブラリ・API、そして、自身の技術力と向き合いながら、「これならまあ、なんとかできるかな」というレベルまで落とし込んでいきます。事前調査は大事です。

ざっくりと整理し、以下の方針で進めることとしました。

  • インストールなどの手間はかけたくないので、Webアプリとして公開
  • Webアプリで画像を扱うのは、情報が充実しているCanvasを利用
  • ペイントツールのような形で描画モードを表示し、キーボードで切り替え可能とする
  • キャンバスを複数個利用し、キーボードで切り替えることで、複数の描画領域をスクロールなしで実現
  • 結果を一気にExcelとして出力できるのが理想だが、実装コストが大きくなりそうなのと、画像の配置の調整はExcelで行った方が良さそうなので、編集結果をクリップボードにコピーすることをゴールとする

このとき、抱えている課題を解決するのはもちろんのことなのですが、趣味の開発なので、「こういうスキルを習得したいな」といった感じの技術的な目標もふんわりと意識しておきます。業務では冒険が難しいことも多々あるかと思いますが、自分一人で好き勝手できるのであれば、スキルアップも目指していきたいです。

ということで、開発を通じて、こんなことを学べたらいいなーという感じの目標をざっくりとたてます。

  • ユーザの入力をもとにキャンバスを動的に描画する機能の実装方法
  • クリップボードを操作するAPIの基本
  • JavaScriptのES2015の基本機能

目標というと、ややハードルが高く見えてしまいますが、狙いとしては、開発を終えたら、上に書いたことができるようになっていたらいいなー、ぐらいのゆるっとした感じです。昨日の自分より強くなりたい。


要件が固まったら、あとはひたすらコードを書いていきます。
12月の土日と年末年始休みがけっこう溶けてなくなりました。

ひたすらコードを書いていったのですが、いきなりアプリがどん、と出来上がったわけではなく、色々と試行錯誤の果てに少しずつ形になっていきました。
普段はフレームワークのお作法に沿って土台となる枠組みから徐々に組み立てていくのですが、今回は、素のJSのみのすごくシンプルなものなので、自分なりに「こう組んでいったらメンテとかしやすくなるんじゃないかな」とあれこれ試しながら進めていきました。

完成形のソースコードにも、もちろんそういった試行錯誤の形跡は含まれているのですが、どういう思考過程でコードが組まれたかを「結果」だけから読み取るのは中々難しいです。
なるべく設計書を書いたり、コメントを多めに書くようにして、「なぜ」を明確にするようには努めました。しかし、せっかくゴリゴリ組んでみたので、ざっくりとどんな風に考えながらコードを組んでいったかを備忘録がてら書いておこうと思います。

※以下の記述が正解というわけではなく、あくまでコードを組むときの思考パターンの一例ぐらいに考えて頂ければと思います。

Step.1 とりあえず組んでみる時期

最初は「超」がつくほどシンプルに、キャンバスに図形を描くことから始めました。
この段階では特にクラス設計とかを意識することもなく、ただひたすらコードを書いて画面で結果を見て...を繰り返していました。いきなりばしっと適切な設計ができれば、試行錯誤の時間も減らせるのですが、まだまだ経験が足りないので、できる部分から取り組んでいきます。

Step.2 「図形」でまとめてみる時期

よくあるオブジェクト指向の解説では、「大きさ」や「色」を持った「図形」クラスを定義し、具体的な図形はそれを継承して使う、といったことが書かれているかと思います。ですが、目に見えるものをそのままプログラムの世界に落とし込むと、管理が難しくなってしまいます。
具象に頼ると直感的に分かりやすくはなりますが、抽象度が下がることで、拡張性・再利用性を失うことになります。

そもそも一かたまりの「状態」と「振る舞い」をまとめて扱えるようにできるのが、クラスのうれしい機能なので、ここでは、図形は、「大きさ」などの状態と、「描画」機能を持ったクラスとして定義します。

例として、「赤枠」を表す図形のクラスを抜粋して記します。

pointRectangle.js(抜粋)
/**
 * 赤枠の四角を表すクラス
 * 位置・大きさ・色を状態として持ち、伸縮を可能とする
 * 
 * @property {string} color 枠の色
 */
export default class PointRectangle extends Shape{

    constructor(context, startX, startY) {

        super(context, startX, startY)
        this.defineAttribute()

        this._color = '#FF0000'
    }

    /**
     * 四角の枠をキャンバス上に描画
     */
    draw() {
        this._context.canvasContext.strokeStyle = this._color
        this._context.canvasContext.strokeRect(this.x, this.y, this.width, this.height)
    }
}

JavaScriptの標準機能ではインタフェースがまだ用意されていないので、擬似的ではありますが、上記のように、各図形にdrawメソッドを用意しておけば、呼び出す側は、図形のdrawメソッドを呼ぶだけで、後は各々がよろしくやってくれる、というようになってくれます。

アプリの中では、配列で図形を管理し、各々を描画するときは、ループでそれぞれのdrawメソッドを呼ぶ形で利用しています。

Step.3 インスタンス管理をいい感じにしたい時期

ある程度機能が充実してくると、インスタンスがもりもり増えてきます。ペイントツールだと特に、「マウスが押されている間」とか「クリックしたとき」など似通った処理をさまざまなインスタンスについて記述する必要が出てくるため、コードの重複がたくさん発生してしまいます。

これを解決するための手法の一例として、まずはコードを見て頂くとイメージが掴みやすいかと思います。

drawingHandler.js(抜粋)
        // アプリで実行され得るイベントの種類
        const occurEvents = ['mousedown', 'mousemove', 'mouseup', 'click']
        // アプリで描画されるオブジェクト
        const drawingList = [
            new MetaDrawing(this._context), new CellDrawing(this._context),
            new RectangleDrawing(this._context), new TextDrawing(this._context), new ImageDrawing(this._context),
            new MoveDrawing(this._context), new DeleteDrawing(this._context), new CursorDrawing(this._context)
        ]

        occurEvents.forEach((event) => {
            const targetEvents = []

            // 各描画機能で扱うイベントを取得
            drawingList.forEach((drawing, index) => {

                if (typeof drawing[`${event}Event`] === 'function') {
                    targetEvents.push(index)
                }
            })

            // イベントリスナーで発火させるべきイベントを設定
            this._context.canvas.addEventListener(event, (eventArg) => {

                targetEvents.forEach((targetIndex) => {

                    // インターセプターで前処理を実行した後、イベント処理を発火
                    drawingList[targetIndex]['setupEvent'].call(drawingList[targetIndex],event, eventArg)
                })
            })
        })

処理されるインスタンスは、キャンバス上でマウス操作をするとなんらかのイベントを発火させるもの、という点で共通しており、似ているものはまとめて処理できるようにしたいです。

そこで、キャンバスで発生し得るイベントを、各々のインスタンスについて、処理を行うかを最初に確認します。処理を行うのであれば、イベントリスナーに登録させます。

そして、イベントが発火したとき、各イベントは、前処理を実現できると便利です。たとえば、削除モードで画面をクリックしたけど、削除対象がなかった場合は、削除処理を実行しない、といったように、イベント後の処理を行うかどうかを判断するための処理などが考えられます。

アプリでは、イベントが発火したタイミングで、直接イベント処理を呼び出すのではなく、前処理として「setupEventメソッド」を呼び出すようにしています。

やや抽象度は上がりましたが、具体的なものとして扱っていると重複していたグループから、「共通化してもグループの本質を崩さない要素」を抽出し、まとめていくことで、機能が増えてもコードをコピーすることなく、一定のルールにもとづいたメソッドを実装するだけで対応できるようになりました。


この他にもクラスが大きくなり過ぎないようレイヤーを分けたりなどなどありましたが、その辺りはまた別の機会に...。

ということで、すごくざっくりとではありますが、実装の中でどういうことを考えながらコードを組んだのかを書き出してみました。長くなってしまいましたが、要点は以下になるかと思います。

  • クラスが扱いにくい or 長くなってきたら、役割単位で統合/切り出しできないか考える
  • 共通化することで可読性・メンテナンス性が損なわれないのであれば、似たものをまとめて扱えないか考える

可読性については、たとえば、二回同じような処理を書いたという理由だけで安直に共通化してしまった場合を考えてみます。
実際はたまたま似ているだけで、別物の処理を一か所に集約させてしまうと、コード量は減ったのに、かえって読みづらくなってしまった、ということが起こってしまいます。
ある程度プログラムを書くことに慣れてくると、なんでも共通化したくなりますが、共通化した結果、読みにくくなっていないかは、常に意識しておくとよいかと思います。

いろいろとエラそうなことを書いてしまいましたが、まだまだ私も修行中の身なので、「私はこういう風に書きました」程度に捉えて頂ければと思います。
また、もっといい書き方あるよ!!等ございましたら、やさしく教えて頂けるとうれしいです。


今後の課題

まずは完成させることを目標としていたので、最初に掲げた「絶対はずせない機能」の実装に注力しました。ですので、当然現状ではまだまだ不十分な点が多々あります。
ここでは、より完成度を高めるために必要な機能を今後の課題として書いていきます。もっと技術力が身についたらリトライしたい...。

  • テキストのフォント・サイズ・色を自由に指定可能に
  • キャンバスを画像ではなく、再操作可能なオブジェクトとして保存し、ブラウザを閉じても再編集可能に
  • アンドゥ・リドゥ機能
  • より精巧なExcelへの擬態

まとめ

思ったよりも色んなところで詰まり、何度か心が折れそうにもなりましたが、なんとか、「こんな課題を解決したいな」というぼやっとした願望から、実際に使えないこともない形にすることができました。内部資料や一回きりの備忘録程度のテスト証跡なんかでは、楽をしていきたいなーと思います。

技術力を身につけるために新しい技術を学ぶのも大事なことではありますが、日常の課題を勉強して培った知識・経験で試行錯誤しながらつくってみるのも、やりがい・達成感があっていいなと思いました。
プログラムを組むための土台も少しずつできてきたかなーと思いたいので、これからも少しずつアプリをつくっていきたいです。頑張りたい。

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