20201010のJavaScriptに関する記事は27件です。

toioの磁気センサーを使って上に載せたものを検出する

サマリ

本記事を読むと、下記の動画の様にtoio™Core Cubeの磁気センサーを使って、キューブの上に載せたものを検出できるようになります。しかもp5.toioを使用しているのでブラウザと磁石があれば、どなたでもお試しは可能です!

  • 例1. ピコトンズの帽子の種類を検出する
  • 例2. ゲズンロイドの紙工作の種類を検出する

成果物

まだ暫定的ですが、p5.toiov0.8.0に入れておきました。ただAPI Referenceはまだないので、すぐにアクセスして使えるサンプルとして、動画にも使った以下の2つも公開いたします。
- p5.toio Sample 11: Magnet function sample1
- p5.toio Sample 12: Magnet function sample2
※ 画面をクリックしてキューブと接続したら、磁石を後述の仕様通りの場所に近づけてください。
449BB54C-D112-4961-92E4-8D6411F05BA7.jpeg

きっかけ

先日、toioの技術仕様書がv2.2.0へ更新され、ゆるゆるとp5.toioに新規機能を追加しています。その中で新たに磁気センサーが追加されたので、そのアプリケーションとして上に載せたものを検出することにしました。今までtoioを使った表現は主としてキューブ下のマットの読み取りを前提として検討をしていたので、上に載せるものが検出できれば、さらに現実世界で表現出来ることが増えるのではないかと思っています。

仕様のおさらい

HW仕様

まずは、公式サイトを見ましょう。
サイトによると、HW仕様としては、4mm*4mm*2mmネオジム磁石について配置をかえることで6種類のパターンで検出が可能となっています。
磁石の位置ですが、縦横方向は画像の通りでわかりやすいですが、高さ方向はキューブから少し距離(トップから3.1mm)をあける必要があるので注意です。(ハマりました)

hardware_magnet.svg
磁石のレイアウト仕様
未装着および以下の仕様に従う 6 つのパターン、合計 7 つパターンを検出できます。
hardware_magnet_layout.svg
The document is licensed under Creative Commons Attribution 4.0 International License.
The image is licensed under Creative Commons Attribution-NoDerivatives 4.0 International License.
Copyright © 2020 Sony Interactive Entertainment Inc.

SW仕様

次にソフト側のBLE制御仕様ですが、こちらも公式サイトをご確認ください。かいつまむと、

  • CharacteristicsはSensor Charを流用する。磁気センサー情報は1st byteで判断する。
  • 後はその他のSensor情報と同様に通知が来る。0x00が未装着、0x01-0x06がそれぞれの装着パターン。
  • なお、基本この機能はオフってあるので、Configuration Charから検出設定をONに変更する必要あり

制御自体はシンプルですが、普段さわらないConfigにアクセスする点は注意ですね。

準備

HW: 磁石

まずは仕様にピッタリの磁石がみつからないと話になりません。その辺に詳しいお友達にヒアリングしたところ、ネオマグさんを紹介されました。結果的に、お値段リーズナブルでほぼ即納、Amazon Pay使えるなどで大変利用しやすかったです。

ターゲットの品はこちらで、仕様通りの1品です。
ネオジム磁石(N45)、角型、4x4x2(mm)、2mm方向
65円/個 (税抜)
ぼくの注文(10個)では、送料の方が高つきました。なお、注文してから、3, 4日で到着しましたと思います。
30F51B07-2629-4895-B673-276E2E089405.jpeg

なななんと、ネオマグさん、2020/10/11-2020/10/31で25周年記念お客様感謝キャンペーンを開催しており、全てのご注文の送料無料だそうです。
マジすか。
もう一回買っておこうかな・・・

なお、Qiitaの読者様へは釈迦に説法かもしれませんが、念のため注意書きを。
今回の磁石は特に寸法が小さくかつ磁力が強いため、取り扱いによっては大変危険なものになりますので、小さなお子様やペットのいるご家庭は十分にケアをして取り扱ってください。飲み込んだりすると一大事ですよ!

HW: スペーサー

前述しましたが、toioのトップ面と磁石の間には3.1mmほどのスペースが必要になります。今回の検出においてこの距離がかなりシビアだったので対応はほぼ必須と考えてください。3Dプリンタとかを持っていれば簡単かもしれませんが、うちにはなく非常に苦労しましたが、解決策を見つけました
なんと、みんな大好きレゴブロックのパネルパーツをスペーサーとして取り付けるだけで、すべて解決しました!
キューブの天面にパネルをつけると、天面からパネルまでの距離が3.2mm位となりますので、ほぼ仕様通りで塩梅が良いです。
5D0F4C0A-5320-49D6-B286-EE4251A5B12B.jpeg

なお、レゴのパネルパーツのサイズですが、基本はキューブ天面の、磁石検出付近のリボン型が割れている箇所にパネルが付いていればよいので、1*2, 1*4, 2*4どれでもOKです。
26BB1CF4-19E3-4314-B710-773DB79945F1.jpeg

ただ、今後キューブに上物をかぶせた時、上物が傾く可能性があるので、下の写真の様に2*4パーツを縦方向に取り付けるか、全面をパネルで覆う方がベターです。
0A20ECAF-DC65-4311-81DA-F34FCAA3AD50.jpeg

このパネルさえあれば、上に磁石を載せるだけで検出できるようになります。

SW: p5.toioの準備

上記仕様で実装するだけだったので、とくにハマりもせず、すぐにp5.js WebEditorなどで動かせるようになりました。
実装を見たい方は、まだmainにマージしていないので、dev-for-v0.8.0ブランチのコレとかコレを参照ください。

また、再掲ですが、アプリとしては下記2つをp5.js WebEditor上に作成しています。
- p5.toio Sample 11: Magnet function sample1:6パターンの磁石検出に応じて、画面に表示されるの背景色と文字を変える。(ピコトンズの帽子や、A-Fキューブスキン検出)
- p5.toio Sample 12: Magnet function sample2:3パターンの磁石検出に応じて、画面に表示される絵文字を変える。(ゲズンロイドの工作物を検出する想定)

検出する

基礎:A-Fキューブスキン検出

まずは仕様通りの磁石配置で機能を確認したいと思います。磁石の検出位置が本当にシビアなので、毎回合わせるのはつらく、パターンごとにキューブスキンを作って磁石を貼り付けることにしました。
こんな感じ。
6742617E-E0EC-4744-A459-4352D1EB4105.jpeg
A314A4B4-F4C7-407C-B47C-438F93A550F6.jpeg

これを実際に検出にかけてみると、、良い感じです。


なかなか反応せず、もどかしい場合や、キューブの個体差があるように感じますが、上々の出来栄えです。

応用1:ゲズンロイド工作検出

上記を応用、というか上に工作物載せただけですが、こういう原理でゲズンロイドの工作検出を実現しました。
キューブの上のレゴパネルは必須です。
31CCC5A3-1AEB-4015-A2E5-B7FAE3C1F885.jpeg

応用2:ピコトンズ帽子検出

こっちは割とテクニカルですが、帽子の中に湾曲させた紙を貼りそこに磁石を取り付けています。
この位置をピンポイントで狙うとパネルパーツなしで検出がうまくいきます。
03088D87-4E21-49A1-8857-270ACD7E8878.jpeg

番外編:おうちにある磁石で検出してみる

家の冷蔵庫にネオジム磁石はたくさん貼ってあるので、それで動作しないのか確認してみました。
結論として、磁力に応じてスペースを変えることを意識すれば、結構問題なく検出できることが分かりました。
これらが、我が家にあったネオジム磁石たち。すべて検出可能でした。
6EF08A07-0D4A-48BC-BE9C-1D1F3295D8BC.jpeg

意外とイケるんだなと。
なお、フェライト磁石は反応しませんでした。

所感と考察

  • 最初にピコトンズ帽子の件を種明かしせずにtwitterにあげましたが、どうなっているの?とびっくりいただけた方もいらっしゃったので、それなりにインパクトはあって良かった。
  • 磁石位置については、かなりシビアで手作業でやるにはちょっと難しく感じました。どなたか3Dプリンタで磁石の凹み付きのデータ公開してくれるとありがたいなと思いました。
  • 磁気センサーはアイディア次第で他にもいろいろできそう。p5.toioなら、ブラウザからアクセスするだけで使えるのでぜひ皆様も。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactコンポーネントの雛形生成を自動化するスクリプトファイルを作る

エンジニアにとって無駄な作業はストレス。
繰り返しの作業を自動化したい。

という事でコンポーネントファイルを自動生成するスクリプトを書いた。

スクリプトファイルの使い方

まずは使い方から。
ルートディレクトリで

bash ./etc/scripts/make-component-template.sh components Layout

というように、ディレクトリ名とコンポーネント名を指定するだけ。


ルートディレクトリ直下にcomponentsディレクトリがある事を想定しています。
carbon (1).png

自動生成するファイル

./etc/scripts/make-component-template.sh components Layoutを実行すると以下のファイルを生成します。

components
└── Layout
    ├── Layout.jsx
    ├── index.jsx
    └── style.css

ファイルの中身

index.jsx
export { default } from './Layout'
Layout.jsx
import React, { memo } from 'react'
import PropTypes from 'prop-types';
import style from './style.css'

Component.propTypes = {

};

const Component = memo(() => {
  return (
    <div>

    </div>
  );
});


function Container(props) {

  return <Component />
}

Container.propTypes = {

};

export default Container

style.cssは空です。

コンポーネントを生成するためのシェルスクリプト

シェルスクリプト

etc/scripts/make-component-template.sh
#!/bin/bash
if [ $# -ne 2 ]; then
  echo "指定された引数は$#個です。" 1>&2
  echo "実行するには2個の引数が必要です。" 1>&2
  echo "例: components(ディレクトリ名) Layout(コンポーネント名)" 1>&2
  exit 1
fi

DIR=$1
COMPONENT=$2
TARGET="$DIR/$COMPONENT"

if [ -e "$TARGET" ]; then
  echo "ディレクトリ'$TARGET'は既に存在します。" 1>&2
  exit
fi

mkdir "$TARGET"
touch "$TARGET/index.jsx"
echo "export { default } from './$COMPONENT'" > "$TARGET/index.jsx"

cp etc/scripts/component-template.txt "$TARGET/$COMPONENT.jsx"

touch "$TARGET/style.css"

Reactコンポーネントのテンプレート

etc/scripts/component-template.txt
import React, { memo } from 'react'
import PropTypes from 'prop-types';
import style from './style.css'


Component.propTypes = {

};

const Component = memo(() => {
  return (
    <div>

    </div>
  );
});



function Container(props) {

  return <Component />
}

Container.propTypes = {

};

export default Container

補足

  • 生成されるReactコンポーネントの構成を変更したい場合は、component-template.txtファイルを書き換えてください。
  • eslintでエラーが出たりprettierで整形されないように、component-templateの拡張子をtxtにしています。

Enjoy Hacking!?

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

【JavaScript】すぐ命令してくる夫に困っています...

3876643_s.jpg

『しかも、何度も同じことばかり命令するんです.....』

if (env === 'test1') {
  if (os === 'ios') {
    setUrl('https://aaa.example.com/apple');
  } else {
    setUrl('https://aaa.example.com/google');
  }
} else {
  if (os === 'ios') {
    setUrl('https://bbb.example.com/appple');
  } else {
    setUrl('https://bbb.example.com/google');
  }
}

・・・

・・・

・・・

読みやすいコードを書きたい

複雑な条件分岐は、書いている本人も、後からそれを読む他人も非常に疲れるものです。
令和プログラマー*1である私自身、なるべく気を付けようと思っていますが、ついつい条件反射でif-elseを書いてしまいそうになります。

また、何度も同じ表現を繰り返すのは、あまり良くないと聞きます。

(*1: 令和になってからプログラミングを知った人。初心者のこと。)

命令ではなく、宣言で書く

if - elseは命令です。 例えるならトロッコに乗るように、線路に沿って進んだ先に分岐があります。
「右へ行け」「左へ行け」とマシンに命令しているのです。

スクリーンショット 2020-07-02 22.34.20.png

では何が辛いかというと、トロッコにずっと乗ってないといけないのが辛いです。

条件を確認して、あっちへ行ったりこっちへ行ったり、時には線路を戻って(アレ? なんの条件にしたがってたんだっけ???)となったり。

常に頭の中でフローを意識しなければならないのは、かなり体力を使います。

宣言で書き直す

必要なデータをあらかじめ用意しておくことで、[if - else]を省略して簡潔に書くことができます。

if文という命令ではなく、変数を宣言し分岐を表現しています。

const url = {
  test1: {
    ios: 'https://aaa.example.com/ios',
    android: 'https://aaa.example.com/apple',
  },
  test2: {
    ios: 'https://bbb.example.com/ios',
    android: 'https://bbb.example.com/google',
  }
}

setUrl(url[env][os]);

でも、なんだかまだ見づらいような...

複数の関心を一度に扱わない

なぜ見づらいかというと、一度の処理で複数の事柄を扱っているからです。

この処理では、大きく2つの分岐を扱っています。

  1. テスト環境(test1 / test2)の分岐
  2. プラットフォーム(ios / android)の分岐

こういった場合、関心をひとつづつ分ける方が読みやすく保守しやすいコードになることがあります。

const host = env === 'test1' ? 'aaa' : 'bbb';
const platform = os === 'ios' ? 'apple' : 'google';

const url = `https://${host}.example.com/${platform}`;
setUrl(url);

最初のコードと比べて、ずいぶん短くすっきりした印象です!

まとめ

命令ではなく、宣言で表現することで、簡潔にできることが多々あるようです。

  • 必要な処理やデータをあらかじめ用意する
  • 複数の関心は、分離する

という意識が必要な気がしています。

いろいろなバリエーションを使いこなして、見通しの良い記述をしたいですね。


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

JavaScriptの非同期処理とコールバック、Promiseについてまとめてみた

はじめに

JavaScriptの非同期処理とコールバック、Promiseについてあまり理解できていなかったので、自分なりに調べてまとめてみました。
間違いがあればコメント頂けると幸いです。

目次

  • そもそも非同期処理とは
  • 非同期処理のサンプル
  • コールバック(Callback)とは
  • コールバック(Callback)の問題点
  • Promiseとは
  • Promiseの書き方
  • 参考

そもそも非同期処理とは

まず、Promiseを知る上で、必要な概念として、非同期処理があります。
非同期処理とは、「ある処理が実行されてから終わるまで待たずに、次に控えている別の処理を行うこと」です。
ちなみに同期処理とは、「ある処理が実行されてから終わるまで、次に控えている処理は待つこと」です。
具体的に非同期処理が必要とされるものとしては、以下の3種類があります。
1. ネットワーク経由のリクエスト(例えばAjax呼び出し)
2. ファイルシステム関連の操作(ファイルの読み書きなの)
3. 意図的に遅延された操作(例えばアラーム)

上記を例えば同期的に処理すると、
1. 読み込みに10分かかるファイルの読み込みを行う。
2. 読み込みを行っている間、JavaScriptアプリケーションは他の処理を行えなくなります。

もし非同期的に処理すると、
1. 読み込みに10分かかるファイルの読み込みを行う。
2. 読み込みを行っている間、JavaScriptアプリケーションは他の処理を行うことができます。

JavaScriptのアプリケーションは「シングルスレッド」で実行されるので、1度にひとつのことを実行していきます。言い換えると、複数の処理を並列で実行することができません。ただ、最近のコンピューターはマルチコアが一般的であり、マルチタスキングが可能であるため、JavaScriptのアプリケーションでも非同期の処理を実現するために、非同期プログラミングの機能が追加されました。

非同期処理のサンプル

単純な例を見ていきます。組み込み関数setTimeoutは指定の時間だけ実行を遅延するものです。実際に実行してみるとコードの記述順とコードの実行順が異なることがわかります。

console.log('setTimoutの前:' + new Date());
function f() {
    console.log('これは関数fの中:' + new Date());
}
setTimeout(f, 10000);/* 10秒後に関数fを実行。コールバック*/
console.log('setTimeoutの後')
console.log('これもsetTimeoutの後')

/* 実行結果
setTimoutの前:Sat Oct 10 2020 18:40:51 GMT+0900 (GMT+09:00)
setTimeoutの後
これもsetTimeoutの後
これは関数fの中:Sat Oct 10 2020 18:41:01 GMT+0900 (GMT+09:00
*/

コールバック(Callback)とは

先ほどのsetTimeout(f,10000);は、10秒後に関数fを呼び出す、というものでしたが、この10秒後に呼び出される関数fをコールバック関数といいます。つまり、「将来のある時点で呼び出される関数」をコールバック関数といいます。

コールバックを利用することで、次のことができるようになります。
1. 非同期処理の中で、決まった順序で処理を実行したい
2. 関数を渡す形にすることで、非同期処理を実行した後の処理の内容を自由に決めたい

コールバック(Callback)の問題点

コールバックを利用することで、非同期の実行の管理ができますが、複数のコールバックを待つ必要があると管理が難しくなる問題点があります。これをコールバック地獄といいます。

/* Nodeで実行 */
const fs = require('fs');

fs.readFile('a.txt', function (err, dataA) {
    if (err) console.error(err);
    fs.readFile('b.txt', function (err, dataB) {
        if (err) console.error(err);
        fs.readFile('c.txt', function (err, dataC) {
            if (err) console.error(err);
            fs.writeFile('d.txt', dataA + dataB + dataC, function (err) {
                if (err) console.error(err);
            });
        });
    });
});

これは3個のテキストファイルを読み込み、それらを1つのファイルに書き出すという処理ですが、深い入れ子のブロックができあがってしまいます。これでは保守性が悪いため、より安全で保守しやすいコードを記述するために、Promiseが考案されました。

Promiseとは

Promiseとは、非同期処理に対するオブジェクトとルールを仕様化することで、複雑な非同期処理をパターンに沿って記述、実行できるようになった仕組みのことをいいます。

先ほどの3個のテキストファイルを読み込み、それらを1つのファイルに書き出すという処理をPromiseに置き換えてみます。

function readFile(file){
    return new Promise((resolve,reject)=>{
        fs.readFile(file,(err,data)=>{
            if(err) {reject(err); return;}
            resolve(data)
        })
    })
}

function writeFile(file, contents){
    return new Promise( (resolve,reject) =>{
        fs.writeFile(file, contents, (err) => {
            if(err) {reject(err); return;}
            resolve("書き込み成功");
        })
    })
}
let totalData;
readFile('a.txt')
    .then((data)=>{
        totalData = data;
        return readFile('b.txt')
    })
    .then((data)=>{
        totalData += data;
        return readFile('c.txt')
    })
    .then((data)=>{
        totalData += data;
        return writeFile('d.txt',totalData)
    })

先ほどよりコードが長くなってしまいましたが、Promiseにした方が見やすくはなったのではないでしょうか。

Promiseの書き方

1. Promiseオブジェクトの作成

Promiseを利用するには、Promiseのオブジェクトを生成していきます。オブジェクトの生成は以下の通りです。
1. new Promise(fn) の返り値がpromiseオブジェクト
2. fn には非同期等の何らかの処理を書く

  • 処理結果が正常なら、resolve(結果の値) を呼ぶ
  • 処理結果がエラーなら、reject(Errorオブジェクト) を呼ぶ

つまり、下記のようなコードになります。

new Promise((resolve, reject) => {
    /* 非同期の処理を記述 */
    setTimeout(() => {
        resolve('Hello World');
    }, 10000);
})

/* 上記をconsole.log()で確認してみた結果は、以下の通りで、promiseオブジェクトが返ってきている。
Promise { <pending> }
*/

2. Promiseオブジェクトへの処理を記述

先ほど作成したPromiseオブジェクトを返す関数を実装してみます。

function asyncFunction() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Hello World');
        }, 10000);
    })
}

asyncFunction()
/* asyncFunction()をconsole.log()で確認してみた結果は、以下の通り。
Promise { <pending> }
*/

ただし、このままですと、戻されるPromiseを何も利用できていないので、Promiseに用意されているthenメソッドを利用していきます。

function asyncFunction() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Hello World');
        }, 10000);
    })
}

asyncFunction().then(
  /* 非同期処理成功時に行う処理を記述 */
  function onFulfilled(value){
    console.log(value)
}).catch(
  /* 非同期処理失敗時に行う処理を記述 */
  function onRejected(err){
    console.log(err)
})

/* 実行結果
10秒後にHello Worldが出力される。
*/

これをアロー関数を使ってみると、以下のかたちで書けます。

function asyncFunction() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Hello World');
        }, 10000);
    })
}

asyncFunction().then((value) => {
    console.log(value)
}).catch((err) => {
    console.log(err)
})

/* 実行結果
10秒後にHello Worldが出力される。
*/

参考

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

kintone-rest-api-clientでゲストスペースを動的に判定してみる

はじめに

kintone-rest-api-clientはとても便利ですが、kintone.api.url()を使うような感覚で、
動的にゲストスペースかどうかの判定が今の所できません。
自社用のカスタマイズであれば問題ないですが、配布を目的としたプラグインなどの場合に少々問題があります・・・。
なので動的に判定できるようにしてみます。

実装

早速実装していきます。
今回はレコード一括取得を例に進めていきます。

    function getAllRecords(){
        return new Promise(async (resolve, reject)=>{
            const guestSpaceId = await checkGuestSpace().catch(err=>{showErrorMsg(err)})
            const prop = guestSpaceId ? {guestSpaceId} : {};
            let client = new KintoneRestAPIClient(prop)

            const params = {
              app: kintone.app.getId(),
              query: kintone.app.getQueryCondition()
            };

            client.record.getAllRecordsWithCursor(params).then(function(resp) {
              console.log(resp);
              resolve(resp)
            }).catch(function(err) {
              console.log(err);
              reject(err)
            });
        })
    }

    async function checkGuestSpace(){
        const currentUrl = kintone.api.url('/k/v1/app', true);
        if(currentUrl.indexOf('guest') > -1){
            const appId = kintone.app.getId();
            const guestSpaceId = await getGuestSpaceId(appId)
            return guestSpaceId;
        } else {
            return false;
        }
    }

    async function getGuestSpaceId(appId){
        const appInfo = await getAppInfo(appId)
        const guestSpaceId = appInfo.spaceId
        return guestSpaceId;
    }

    function getAppInfo(appId){
        return new Promise ((resolve, reject)=>{
            const body = {
              'id': appId
            };
            kintone.api(kintone.api.url('/k/v1/app', true), 'GET', body, function(resp) {
              resolve(resp)
            }, function(error) {
              reject(error)
            });
        })
    }

ゲストスペースかどうかを判定する

kintone.api.url()を使って、適当なAPIを叩いてURLを取します。
「guest」が含まれているかどうかを判定基準としています。

const currentUrl = kintone.api.url('/k/v1/app', true);
if(currentUrl.indexOf('guest') > -1){

ゲストスペースであった場合は、ゲストスペースIDを取得します
(kintone-rest-api-clientでは、ゲストスペースで使用する場合ゲストスペースIDを指定する必要があります)
アプリ情報取得APIでアプリ情報を取得し、ゲストスペースIDを取り出します。

ゲストスペースIDが取得できたらguestSpaceIdプロパティを設定、なければ何もしないといった感じです。

const prop = guestSpaceId ? {guestSpaceId} : {};

最後に

とりあえずぱぱっと書きましたが、レコード一括取得以外でも使えるように工夫が必要ですね。
コードの書き方などご指摘大歓迎ですm(_ _)m

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

[初心者向け]Vue.js 重要単語集

Vue.js

  • 簡単に言うと、ユーザーインターフェイスを構築するためのフレームワークのこと
  • JavaScriptでDOMを操作するWebアプリケーションを構築するときに向いている
  • DOMよりも先にデータが存在していて、それに合わせてDOMが構築される
  • HTMLベースのテンプレート構文を使っているので、Vueインスタンスのデータと描画をDOMを宣言的に対応させることができる

Vueインスタンス

var app = new Vue({
  //下記などのオプションを記述する
})

el

アプリケーションを紐付ける要素のセレクタ

data

アプリケーションで使用するデータ、オブジェクトや配列も登録できる

computed

dataと似たように扱うことのできる、関数によって算出されたデータ、算出プロパティ

mothods

このアプリケーションで使用するメソッド、コードを管理しやすくするために処理を分けたり、イベントハンドラなど細かな実装を担当する

DOM

  • JavascriptからHTMLやCSSのデータを取得、操作するための仕組み

データバインディング

  • データと描画を同期させる仕組みのこと
  • JavaScriptとのデータとそれを使用する場所を紐付け、データに変化があれば自動的にDOMを更新する

ディレクティブ

  • 接頭辞「 v- 」が付いたVue.jsの特別な属性のことで、テンプレートとロジックを関連付ける (下記が代表的なもの)

v-for

配列やオブジェクトから要素を繰り返し描画する

v-on

DOMイベントのハンドリングに使う

v-model

データとフォームの入力項目のバインド(関連付け)をする

v-if

テンプレートベースで条件分岐をする

コンポーネント

  • 再使用可能なVueインスタンスを作ること
  • 機能ごとにJavaScriptとテンプレートを1つのセットにして、他の機能とは分離して開発できるようにする仕組み
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactHookFormの導入と簡単な使い方

はじめに

今回はReactHookFormの導入と簡単な解説をしていこうと思います。
Reactの環境構築はできているものとします。一応、JavasciptとTypescriptの両方のコードを載せているので自分が使っている方を見てください。

参考:ReactHookFormドキュメント

ReactHookFormとは

ReactHookFormでは、Form内のデータをStateで管理する必要が無くなり、onChangeなどによるレンダリング回数を劇的に減らすことができます。(useCallbackとかでも減らせるっぽい)
さらにバリーデーションも簡単に行うことができます。
早速やっていきましょう!

導入

VSCodeのターミナルなどで以下のコマンドを入力しましょう。

VSCodeのターミナル
# npm
npm install react-hook-form

# yarn
yarn add react-hook-form

これでもう使えるようになります。Typescriptの型定義ファイルも一緒に入ってるのでそのまま使えるみたいです!

簡単な使い方

以下のようなonChangeでvalueを更新して、onSubmitでvalueを表示する簡単なプログラムを書き換えてみましょう!

App.jsx
import React, { useState } from 'react';

export const App = () => {
    const [value, set_value] = useState('');
    const [text, set_text] = useState('');

    const handle_change = (e) => {
        set_value(e.target.value);
    };

    const handle_submit = (e) => {
        e.preventDefault();
        set_text(value);
        set_value('');
    };

    return (
        <>
            <form onSubmit={handle_submit}>
                <input type="text" value={value} onChange={handle_change} />
                <button type="submit">追加</button>
            </form>
            <h1>{text}</h1>
        </>
    );
};

↓以下のように書き換えました!バリデーションも追加しています。

Javascript

App.jsx
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';

export const App = () => {
    const [text, set_text] = useState('');
    const { register, errors, handleSubmit, reset } = useForm();

    const handle_submit = (data) => {
        set_text(data.value);
        reset();
    };

    return (
        <>
            <form onSubmit={handleSubmit(handle_submit)}>
                <input
                    type="text"
                    name="value"
                    ref={register({ required: 'テキストを入力してください' })}
                />
                <button type="submit">追加</button>
            </form>
            <h1>{text}</h1>
            {errors.value && <p>{errors.value.message}</p>}
        </>
    );
};

Typescript

App.tsx
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';

type FormData = {
    value: string;
};

export const App = () => {
    const [text, set_text] = useState('');
    const { register, errors, handleSubmit, reset } = useForm<FormData>();

    const handle_submit = (data: FormData) => {
        set_text(data.value);
        reset();
    };

    return (
        <>
            <form onSubmit={handleSubmit(handle_submit)}>
                <input
                    type="text"
                    name="value"
                    ref={register({ required: 'テキストを入力してください' })}
                />
                <button type="submit">追加</button>
            </form>
            <h1>{text}</h1>
            {errors.value && <p>{errors.value.message}</p>}
        </>
    );
};

上から解説していきます!
まず、react-hook-formからuseFormを名前付きインポートします。

import { useForm } from 'react-hook-form';

useFormから今回使うregister、errors、handleSubmit、resetを分割代入します。

名前 役割
register form内のinputなどの参照、バリデーションなど
errors エラーの表示
handleSubmit formの入力内容を取得
reset form内の入力内容のリセット
const { register, errors, handleSubmit, reset } = useForm();

dataという名前で入力されたデータをとってきて、set_textに入れています。

console.logすればわかるのですが、dataは以下のようなオブジェクトになっています。

data: {
  value: // 入力内容
}

valueとなっているのはinputのname属性を参照しているからです。

その後、resetで入力内容をリセットしています。

const handle_submit = (data) => {
        set_text(data.value);
        reset();
};

先ほど書いたように、name属性にvalueを定義しています。ref属性にregisterを書く必要があり、その後に({})みたいな感じで連想配列が作れて、そこでバリデーションを定義することができます。

errorsのところは入力したvalueがregisterのバリデーションに引っ掛かった時にエラーメッセージを表示する処理を書いています。

return (
        <>
            <form onSubmit={handleSubmit(handle_submit)}>
                <input
                    type="text"
                    name="value"
                    ref={register({ required: 'テキストを入力してください' })}
                />
                <button type="submit">追加</button>
            </form>
            <h1>{text}</h1>
            {errors.value && <p>{errors.value.message}</p>}
        </>
    );

valueをStateで管理する必要が無くなり、バリデーションも簡単に実装できました!

最後に

ここまで読んでいただきありがとうございました!ReactHookFormについて少しでもわかっていただけたら幸いです!
質問やご要望などありましたら、コメントしていただけるとモチベーションにつながります!
今回のでは、入力してない時にボタンをdisableにしたりみたいなのができないので、次はReactHookFormで入力の有無の状態を取得する方法をまとめたいと思います。

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

Vue + Wordpress + Heroku + S3でポートフォリオを構築する

1. はじめに

こんにちは。ツダと申します。私はカメラが趣味で、自分の写真のポートフォリオサイトを作成したいと思い、Vue.jsとWordpressを使って作成しました。

Tsuda Work

この記事では、私がポートフォリオを作るうえで行ったことについて紹介させていただければと思っています。

2. 技術スタック

  • Vue.js : @vue/cli 4.5.6
  • Wordpress : 5.5.1
  • PHP : 7.3.5
  • heroku : heroku/7.44.0 win32-x64 node-v12.16.2
  • S3

簡単なシーケンス図(トップ画面表示時)

Tsudawork.png

3. Wordpress環境を整える

3.1 概要

今回のポートフォリオでは、WordpressをAPIとして使用します。 ですので、フロントエンド側はVueを、バックエンド側はWordpressというような役割分担をするイメージです。

参考:WP REST API

3.2 Localbyflywheelをダウンロード

まずはWordpress環境を構築します。環境構築にはLocalbyFlywheelを用います
。環境構築方法については以下のサイトを参考にしていただければと思います。

3.3 DB設定

後述しますが、デプロイするサーバは「heroku」を使います。herokuで使用するDBとローカル環境で使用するDBが異なりますので、wp-config.phpに条件分岐を書きます。wp-config.phpは作成したWordpressプロジェクトのルートフォルダに配置してあります(Windowsの場合、C:\Users\<ユーザ名>\Local Sites\<作成したアプリ名>\app\public配下)

wp-config.php
...(省略)...

// ** MySQL settings - You can get this info from your web host ** //
if (
    @$_SERVER["SERVER_NAME"] === '<作成したWordpressのローカル環境のドメイン名>'
) {
    // ローカル環境の設定
    define( 'DB_NAME', 'local' );
    define( 'DB_USER', 'root' );
    define( 'DB_PASSWORD', 'root' );
    define( 'DB_HOST', 'localhost' );
} else {
    // heroku環境の設定
    $url = parse_url(getenv('CLEARDB_DATABASE_URL'));
    define('DB_NAME', trim($url['path'], '/'));
    define('DB_USER', $url['user']);
    define('DB_PASSWORD', $url['pass']);
    define('DB_HOST', $url['host']);
}
define( 'DB_CHARSET', 'utf8' );
define( 'DB_COLLATE', '' );

...(省略)...

参考:PaaS入門 〜Heroku + wordpress〜

3.4 セキュリティ面の設定

WordPressはあくまでAPIとしての役割で使用するため、WordPressサイトにはアクセスされたくありません。そこで、直接アクセスされるのを防ぐための各種設定を行います。

ログイン画面のパスを変更する

WordPressはログイン画面へのパスがデフォルトで "/wp-admin" として設定されています。しかし、これではログイン画面へのアクセスが容易にできてしまうので、これを変更するためのプラグイン「Login Rebuilder」が用意されています。

参考: Login Rebuilderの使い方|WordPressのログインURLの変更方法

サイトへのアクセスを制御する

次にサイトへのアクセスを制御します。何も設定していない場合、サイトへアクセスした際に記事が表示されてしまいますので、表示されないように設定します。

設定するためには、設定しているテーマのfunctions.phpに下記のコードを追加します。

functions.php
function access_restriction() {
    // 許可するアクセス
    // 管理画面
     if (is_admin()) {
         return;
    }
    // APIのアクセス
    if (strpos($_SERVER['REQUEST_URI'],'wp-json')) {
       return;
    }
    // ログイン画面
    if (strpos($_SERVER['REQUEST_URI'],'<Login Rebuilderで設定したログイン画面へのパス>')) {
       return;
    }
     wp_die('アクセスできません', 'アクセス拒否', array('responce' => 403));
 }
 add_action('init', 'access_restriction');

管理画面、APIへのアクセス、およびログイン画面へのアクセスは必要なので許可し、それ以外を拒否するように設定しています。実際にアクセスしてみると下のような拒否画面が現れます。

WS000021.JPG

参考:WordPress へのアクセス制限を functions.php で設定する

3.5 S3を使うための設定をする

Herokuは定期的にビルドを行っているため、アップロードした画像が消えてしまう現象が発生します。

herokuのリポジトリがdynoという単位で管理されており、Freeプランだと一定時間経過後にdynoが再起動される仕様になっているから。 - 【Rails6】herokuで投稿した画像が表示されない

そこでAWSの「S3」というサービスを用いることで画像が消える現象を回避していきます。Wordpressのプラグインを用いてアップロードする画像の格納先をS3に設定することで、外部に画像を保存します。

S3を使用するための準備をする

参考:WordPressの画像データをS3に保存する

S3のアクセス権限の設定

ブロックパブリックアクセスは下記のように設定。
WS000022.JPG
この設定によって以下の二つを達成することを確認しております。

  • WordPressからの画像アップロード
  • ポートフォリオサイトからの画像の表示

参考:Amazon S3 パブリックアクセスブロックの使用

バケットポリシーの設定は次のようにしています。

バケットポリシー
{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "AllowPublicRead",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "<リソース名>/*"
        }
    ]
}

参考:

ローカル環境でS3へつなげるようにする

私がローカル環境でテストを行った際、WordpressからS3につなげず、以下のようなエラーが発生しました。

Error Getting Bucket Region —There was an error attempting to get the region of the bucket tsudawork: Error executing "GetBucketLocation" on "<バケットのURL>?location"; AWS HTTP error: cURL error 60: (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)

内容はCURLでS3へつなぐ際にコード60のエラーが発生しているというものです。コード60は次のようになっています。

CURLE_PEER_FAILED_VERIFICATION (60)
The remote server's SSL certificate or SSH md5 fingerprint was deemed not OK. This error code has been unified with CURLE_SSL_CACERT since 7.62.0. Its previous value was 51. - libcurl error codes

これだけでは少しわかりづらいですが、調べていると次のような記述を発見しました。

OSにバンドルされているルート証明書などが古いので証明書の検証ができない様子。 - curl: (60) SSL certificate problem, verify that the CA cert is OK

私の解釈ですが、つなぎ先サーバのSSL証明書とつなぎ元端末のSSL証明書を検証した際、つなぎ元端末のSSL証明書が存在しないor古いために発生するエラーと考えられます。こちらは次の対策をすることで解決しました。

まず、次のサイトより最新の証明書(cacert.pem)をダウンロードします。

CA certificates extracted from Mozilla

次に作成したwordpressプロジェクトのconf/phpフォルダ配下にphp.ini.hbsというファイルがありますので、そのファイルの一番下に次のコードを追加します。

php.ini.hbs
[curl]
curl.cainfo = "<ダウンロードしたcacert.pemのパス>"

Wordpressプロジェクトを再起動し、WP Offload Media Liteの設定画面でS3のバケットを選択すると接続ができるようになっているはずです。

参考:
curl: (60) SSL certificate problem: unable to get local issuer certificate
cURL error 60: SSL certificate problem: certificate has expired

GDをComposerに追加する

WordpressでS3を使用するためにはGD(画像ファイルの操作ライブラリ)の使用が必要です。HerokuにはデフォルトでGDがインストールされてないため、Composerで使えるように設定する必要があります。

参考:Image Processing and GD

まず、composer.jsonに下記のように追記します。

composer.json
{
    "require": {
        "ext-gd": "*"
    }
}

作ったファイルはwordpressのルートディレクトリに配置します。続いて下記のコマンドを実行します。

$ composer install

終了後、composer.lockがcomposer.jsonと同じディレクトリに作成されていればOKです。後はHerokuへのデプロイ時にHerokuが自動的にライブラリをインストールします。

参考:Heroku PHP Support

3.6 Herokuへデプロイする

まずは下記の記事を参考に、Herokuが使える状態にします。

参考:Heroku初心者がHello, Herokuをしてみる

そして下記の記事を参考に、作成したWordpressプロジェクトをHerokuへデプロイします。

参考:PaaS入門 〜Heroku + wordpress〜

主に次のことをやる必要があります。

  • Wordpressのルートディレクトリで「git init」+「git commit -m "commit"」
  • Wordpress用のプロジェクトアプリ作成
  • composer.jsonおよびcomposer.lockの作成
  • DB作成
  • デプロイ

3.7 パーマリンクの設定

WP REST APIにリクエストを投げると、デフォルトの状態では404エラーが返ってくるそうです。そのためにパーマリンクの設定をしてあげます。

※私の環境でこの設定をしなかったところ、 管理画面からの記事の編集・投稿ができませんでしたので設定することがおすすめです。

設定方法

設定方法はいたって簡単です。Wordpressの管理画面左にあるタブから「設定→パーマリンク設定」と進み、共通設定の「投稿名」にチェックをいれるだけとなっております。

WS000030.jpg

これで404エラーの原因となるパーマリンク設定が解消されるはずです。

参考:WP REST APIで404が返ってくる。これはパーマリンク設定のせいだ!

4. Vue.jsでプロジェクトを作成していく

4.1 環境構築

まずVueの環境構築を行います。Vueを使用する方法は主に二つあります

  • CDN
  • Vue CLI

ここではVue CLIを用いた方法を選択します。「参考」に記載させていただいた記事をもとに、「Welcome to Your Vue.js App」が表示されればOKです。

参考:Vue.js を vue-cli を使ってシンプルにはじめてみる

4.2 必要なモジュールをインストール

  • axios
$ npm install axios
  • bootstrap-vue
$ npm install vue bootstrap-vue bootstrap
  • vue-router
$ npm install vue-router

参考:
axios - npm
Getting Started | BootstrapVue
Installation | Vue Router

4.3 アプリの全体像

アプリの全体像は次のようになっています。

main.js
import Vue from 'vue'
import Articles from './Articles.vue'
import Article from './Article.vue'
import Router from 'vue-router'
import Top from './Top.vue'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

// Router
Vue.use(Router)

// Install BootstrapVue
Vue.use(BootstrapVue)

// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)


const router = new Router({
  mode:'history',
  routes: [
    {
      path: '/',
      component: Articles
    },    
    {
      path: '/category/:value',
      name: 'category',
      component: Articles,
    },
    {
      path: '/post/:value',
      name: 'post',
      component: Article
    }
  ]
})

Vue.config.productionTip = false

new Vue({
  render: h => h(Top),
  router
}).$mount('#app')
Top.vue
<template>
  <div id="app">
    <b-navbar class="navbar bg-white" fixed="top" toggleable="lg">
      <b-navbar-brand href="/">Tsuda Work</b-navbar-brand>
      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item-dropdown :text="category.name" class="nav-item" v-for="category in categories" :key="category.id">
              <b-dropdown-item><router-link class="nav-link" :to="{name:'category', params:{value:category.id}}">All</router-link></b-dropdown-item>
              <b-dropdown-item :text="subCategory.name" class="nav-item" v-for="subCategory in category.subCategories" :key="subCategory.id">
                <router-link class="nav-link" :to="{name:'category', params:{value:subCategory.id}}">{{subCategory.name}}</router-link>
              </b-dropdown-item>
          </b-nav-item-dropdown>
        </b-navbar-nav>
        <b-navbar-nav class="ml-auto">
          <b-nav-item class="nav-item" href="https://twitter.com/tsuda215">
            <div>Twitter</div>
          </b-nav-item>   
        </b-navbar-nav>
      </b-collapse>
    </b-navbar> 
    <router-view class="article-router" :key="$route.path"></router-view>
    <footer class="page-footer font-small blue pt-4">
      <div class="footer-copyright text-center py-3">© 2019-2020 Tsuda Work.</div>
    </footer>
  </div>
</template>

<script>
import axios from 'axios';
import states from "./assets/property.json";
export default {
  name: 'Top',
  data() {
      return {
        categories: [],
      }
  },
  mounted() {
    const url = states.hostname + states.categoriesUrl;
    (async () => {
      try {
        // カテゴリー取得
        const res = await axios.get(url);
        var categoriesTmp = [].concat(res.data);
        for (var categoryTmp of categoriesTmp) {
          // サブカテゴリ―の場合は処理をスキップ
          if (categoryTmp.parent > 0) {
            continue;
          }

          // カテゴリー作成
          var category = {};
          category['id'] = categoryTmp.id;
          category['name'] = categoryTmp.name;
          category['subCategories'] = [];

          // サブカテゴリー取得
          const resForSubCategories = await axios.get(url + "?parent=" + categoryTmp.id);
          var subCategoriesTmp = [].concat(resForSubCategories.data);

          for (var subCategoryTmp of subCategoriesTmp) {
            // サブカテゴリー作成
            var subCategory = {};
            subCategory['id'] = subCategoryTmp.id;
            subCategory['name'] = subCategoryTmp.name;
            // サブカテゴリ―配列に追加
            category['subCategories'].push(subCategory);
          }

          // カテゴリー配列に追加
          this.categories.push(category);
        }
      } catch (error) {
        console.log(error);
      }
    })();
  },
}
</script>

<style>
#app {
  display: flex;
  flex-direction: column;
  min-height: 82vh;
  font-family: "Yu Gothic","Yu Gothic UI","Meiryo UI", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "MS PGothic", sans-serif; 
}
.navbar {
  background-color:rgba(0, 0, 0, 0);
}
.page-footer {
  margin-bottom:0;  
}
@media screen and (orientation:portrait) {
  .article-router {
    margin-top:10vh;
  }
}
@media screen and (orientation:landscape) {
  .article-router {
    margin-top:7vw;
  }
  @media screen and (max-width: 800px){
    .article-router{
      margin-top:9vw;
    }
  }
}
</style>
Articles.vue
<template>
  <div id="app">
    <transition name="fade">
      <!-- フェードイン実装のためv-ifは必要 -->
      <div v-if="ok" class="album py-5">
        <div class="container">
          <div class="row">
            <div class="col-md-4 mb-4" v-for="post in posts" :key="post.title.rendered">
              <div class="card h-100 shadow-sm">
                <router-link class="nav-link" :to="{name:'post', params:{value:post.id}}">
                    <img class="card-img-top" :src="post._embedded['wp:featuredmedia'][0].source_url" alt="">
                </router-link>
              </div>
            </div>
          </div>
        </div>
      </div>
    </transition>
  </div>
</template>

<script>
import axios from 'axios';
import states from "./assets/property.json";
export default {
  name: 'Articles',
  data() {
      return {
        posts: [],
        // フェードイン実装のために必要
        ok: false
      }
  },
  mounted() {
    var categoryId = this.$route.params.value;
    var url = states.hostname + states.postsUrl;
    if (categoryId != undefined) {
      url = states.hostname + states.categoryUrl + categoryId + '&_embed';
    }
    (async () => {  
      try {
        const res = await axios.get(url);
        this.posts = this.posts.concat(res.data);
        // マウント時にok=trueを実施
        this.ok = true;
      } catch (error) {
        console.log(error);
      }
    })();
  },
}
</script>

<style>
.card {
  display: flex;
  justify-content: center; /*左右中央揃え*/
  align-items: center; /*上下中央揃え*/
}

.fade-enter-active, .fade-leave-active {
  transition: opacity 1s;
}

.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}
</style>
Article.vue
<template>
  <transition name="fade">
    <div v-if="ok" class="container">
      <div class="row">
        <div class="articleContainer">
          <div class="article">
            <h3 class="title">{{post.title.rendered}}</h3> 
            <div class="content" v-html="post.content.rendered">
            </div>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
import axios from 'axios';
import states from "./assets/property.json";
export default {
  name: 'Article',
  data() {
      return {
        post: null,
        // フェードイン実装のために必要
        ok: false
      }
  },
  mounted() {
    var postId = this.$route.params.value;
    const url = states.hostname + states.postUrl + postId + '?_embed';
    (async () => {
      try {
        const res = await axios.get(url);
        this.post = res.data;
        // マウント時にok=trueを実施
        this.ok = true;
      } catch (error) {
        console.log(error);
      }
    })();
  },
}
</script>

<style>
.articleContainer {
  display: flex;
  justify-content: center; /*左右中央揃え*/
  align-items: center; /*上下中央揃え*/
  width: 100%;
}

.article > .title {
  text-align: center;
}

.article img {
  width: 100%;
}

@media screen and (min-width: 800px){
    .article{
        width:60%;
    }
}

@media screen and (max-width: 799px){
    .article{
        width:85%;
    }
}

/* フェードインの設定 */
.fade-enter-active
/* , .fade-leave-active */
{
  transition: opacity 2s;
}
.fade-enter
/* , .fade-leave-to */
{
  opacity: 0;
}
.fade-leave-active {
  display:none;
}

</style>
property.json
{
    "hostname":"https://<Wordpressプロジェクトのホスト名>/",
    "postUrl":"wp-json/wp/v2/posts/",
    "postsUrl":"wp-json/wp/v2/posts?_embed",
    "categoryUrl":"wp-json/wp/v2/posts?categories=",
    "categoriesUrl":"wp-json/wp/v2/categories"
}

※注意

  • ホスト名を間違えると"No 'Access-Control-Allow-Origin' header is present on the requested resource."という旨のエラーが出ることを確認しました。
  • httpsをhttpにして設定すると、Heroku上のVueから画像が取得できないことを確認しました。

4.4 アプリの解説

Navbar

Navbarを使用する前に、vueでbootstrapを使用できるようにする必要があります。

参考:Getting Started | BootstrapVue

インストールコマンドは次の通りです。

# With npm
npm install vue bootstrap-vue bootstrap

# With yarn
yarn add vue bootstrap-vue bootstrap

インストールした後はmain.jsにてbootstrapを使えるように設定しています。

main.js
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

...(省略)...

// Install BootstrapVue
Vue.use(BootstrapVue)

// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)

次にメニュー部分の実装に入ります。
参考:Navbar

Top.vue
<template>
  <div id="app">
    <b-navbar class="navbar bg-white" fixed="top" toggleable="lg">
      <b-navbar-brand href="/">Tsuda Work</b-navbar-brand>
      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item-dropdown :text="category.name" class="nav-item" v-for="category in categories" :key="category.id">
              <b-dropdown-item><router-link class="nav-link" :to="{name:'category', params:{value:category.id}}">All</router-link></b-dropdown-item>
              <b-dropdown-item :text="subCategory.name" class="nav-item" v-for="subCategory in category.subCategories" :key="subCategory.id">
                <router-link class="nav-link" :to="{name:'category', params:{value:subCategory.id}}">{{subCategory.name}}</router-link>
              </b-dropdown-item>
          </b-nav-item-dropdown>
        </b-navbar-nav>
        <b-navbar-nav class="ml-auto">
          <b-nav-item class="nav-item" href="https://twitter.com/tsuda215">
            <div>Twitter</div>
          </b-nav-item>   
        </b-navbar-nav>
      </b-collapse>
    </b-navbar> 
    <router-view class="article-router" :key="$route.path"></router-view>
    <footer class="page-footer font-small blue pt-4">
      <div class="footer-copyright text-center py-3">© 2019-2020 Tsuda Work.</div>
    </footer>
  </div>
</template>

ポイントは次の部分です。

Top.vue
<b-nav-item-dropdown :text="category.name" class="nav-item" v-for="category in categories" :key="category.id">
    <b-dropdown-item><router-link class="nav-link" :to="{name:'category', params:{value:category.id}}">All</router-link></b-dropdown-item>
    <b-dropdown-item :text="subCategory.name" class="nav-item" v-for="subCategory in category.subCategories" :key="subCategory.id">
      <router-link class="nav-link" :to="{name:'category', params:{value:subCategory.id}}">{{subCategory.name}}</router-link>
    </b-dropdown-item>
</b-nav-item-dropdown>

親カテゴリはAllとして必ず作成するようにしています。そしてサブカテゴリの数だけループを回し、追加のドロップダウンアイテムを作成しています。カテゴリ作成ロジックは次のようにしています。

Top.vue
mounted() {
  const url = states.hostname + states.categoriesUrl;
  (async () => {
    try {
      // カテゴリー取得
      const res = await axios.get(url);
      var categoriesTmp = [].concat(res.data);
      for (var categoryTmp of categoriesTmp) {
        // サブカテゴリ―の場合は処理をスキップ
        if (categoryTmp.parent > 0) {
          continue;
        }

        // カテゴリー作成
        var category = {};
        category['id'] = categoryTmp.id;
        category['name'] = categoryTmp.name;
        category['subCategories'] = [];

        // サブカテゴリー取得
        const resForSubCategories = await axios.get(url + "?parent=" + categoryTmp.id);
        var subCategoriesTmp = [].concat(resForSubCategories.data);

        for (var subCategoryTmp of subCategoriesTmp) {
          // サブカテゴリー作成
          var subCategory = {};
          subCategory['id'] = subCategoryTmp.id;
          subCategory['name'] = subCategoryTmp.name;
          // サブカテゴリ―配列に追加
          category['subCategories'].push(subCategory);
        }

        // カテゴリー配列に追加
        this.categories.push(category);
      }
    } catch (error) {
      console.log(error);
    }
  })();
},

記事表示部分

記事の表示は次の部分です。

Articles.vue
<div class="container">
  <div class="row">
    <div class="col-md-4 mb-4" v-for="post in posts" :key="post.title.rendered">
      <div class="card h-100 shadow-sm">
        <router-link class="nav-link" :to="{name:'post', params:{value:post.id}}">
            <img class="card-img-top" :src="post._embedded['wp:featuredmedia'][0].source_url" alt="">
        </router-link>
      </div>
    </div>
  </div>
</div>

こちらもbootstrapの使用が必要になります。

フェードインの実装

記事の一覧を表示する際にフェードインを使用しています。
参考:Enter/Leave & List Transitions

フェードインの実装にはvueのtransitionという機能を使用しています。transitionを使用するためにはフェードさせたい要素をで囲む必要があります。

Articles.vue
<transition name="fade">
  <!-- フェードイン実装のためv-ifは必要 -->
  <div v-if="ok" class="album py-3">
...(省略)...
  </div>
</transition>

v-ifに指定の"ok"はデフォルトでfalseとなっています。

Articles.vue
data() {
    return {
      posts: [],
      // フェードイン実装のために必要
      ok: false
    }
},

そしてマウント時にtrueとすることで、要素を表示させます。

Articles.vue
mounted() {
...(省略)...
    (async () => {  
      try {
        const res = await axios.get(url);
        this.posts = this.posts.concat(res.data);
        // マウント時にok=trueを実施
        this.ok = true;
      } catch (error) {
        console.log(error);
      }
    })();
  },

表示の際は下記のcssを使用しているので、フェードインでの表示となります。

Articles.vue
<style>
...(省略)...
.fade-enter-active, .fade-leave-active {
  transition: opacity 1s;
}

.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}
</style>

Wordpress REST APIを用いて記事取得

記事を取得する際はWordpress REST APIにリクエストを投げますが、リクエストを投げる際はaxiosを使用しています。

Articles.vue
<script>
import axios from 'axios';
import states from "./assets/property.json";
export default {
  name: 'Articles',
  data() {
      return {
        posts: [],
        // フェードイン実装のために必要
        ok: false
      }
  },
  mounted() {
    var categoryId = this.$route.params.value;
    var url = states.hostname + states.postsUrl;
    if (categoryId != undefined) {
      url = states.hostname + states.categoryUrl + categoryId + '&_embed';
    }
    (async () => {  
      try {
        const res = await axios.get(url);
        this.posts = this.posts.concat(res.data);
        // マウント時にok=trueを実施
        this.ok = true;
      } catch (error) {
        console.log(error);
      }
    })();
  },
}
</script>

実際にリクエストを投げているのは次の部分です。

const res = await axios.get(url);

記事のIDを取得したときなど、単数を指定するものはObject、IDを指定せず記事を複数取得した場合はObjectの配列がres.dataには格納されています。後は取得したものを加工し、vueで描画すればOKです。

参考:《WordPress》2017年末にWP REST API で取得してVue.jsで描画するまでのまとめ。

4.5 アプリをHerokuにデプロイする。

「参考」に記載の記事をもとに、大まかには下記の作業を行う必要があります。

  • expressのインストール
  • server.jsの作成
  • デプロイ

これらの作業を完了しアプリを表示すると、Wordpressで作成したアプリに対してリクエストが飛んでいるはずです。無事記事の一覧が表示されていればOKです。

参考:Vue.jsで作ったアプリをHerokuにデプロイ

4.6 リロードの設定

Vueアプリはデフォルトでrouterのパスに「ハッシュ(#)」が含まれています。これはrouterを「historyモード」に変更することで削除することができますが、historyモードを使用することでページをリロードした際に、404エラーが返却されてしまいます(historyモードでない場合にはこの問題は発生しません)。

しかしながら一点問題があります。シングルページのクライアントサイドアプリケーションなので、適切なサーバーの設定をしないと、ユーザーがブラウザで直接 http://oursite.com/user/id にアクセスした場合に 404 エラーが発生します。- HTML5 History モード

しかし、適切な設定をすれば「ハッシュを削除しつつ、リロードにも対応する」ことが可能となります。

connect-history-api-fallbackをインストールする

npm install --save connect-history-api-fallback

参考:connect-history-api-fallback

server.jsにコードを追加する

server.js

var express = require('express');
var path = require('path');


+ const history = require('connect-history-api-fallback');


var serveStatic = require('serve-static');
app = express();


+ app.use(history());


app.use(serveStatic(__dirname + "/dist"));
var port = process.env.PORT || 5000;
app.listen(port);
console.log('server started '+ port);

こちらローカル環境では動作しておりませんが、Herokuでは動作することを確認しております。

参考:vue-routerのルーティングURLからハッシュを除去しつつ、URL直接指定でも表示させる(Node, Express)

5. その他

5.1 スリープ防止のため、スケジューラ作成

Herokuのサイトは一定時間が経過すると、スリープ状態に入ってしまうため、次にアクセスする際にサイトが表示されるまで時間がかかるという問題が発生してしまいます。ここではそれを防止する方法について紹介します。

参考:herokuを24時間稼働させる設定

herokuにスケジューラを追加

まずはアプリケーションフォルダ配下で下記コマンドを実行し、スケジューラのアドオンを追加していきます。

$ heroku addons:create scheduler:standard

下記のように表示されれば成功です。

Creating scheduler:standard on ⬢ <アプリ名>... free
 To manage scheduled jobs run:
 heroku addons:open scheduler

Created <スケジューラの名前>
Use heroku addons:docs scheduler to view documentation

ジョブを作成する

続いて下記コマンドを実行して、ジョブの作成画面を表示します。

$ heroku addons:open scheduler

すると下記のような画面が表示されるはずです。

WS000013.jpg

「Create Job」を押下します。

WS000018.jpg

すると下記のようなウィンドウが表示されますので、

Schedule : Every 10 minutes
Run Command : curl <サイトのURL>

を入力し、「Save Job」を押下します。
これでジョブの作成は完了です。後は時間をおいてもう一度サイトにアクセスしてみると、表示が遅い問題が解消されているはずです。

5.2 Wordpressでアップロードできる画像の大きさを上げる

HerokuにWordpressをデプロイした直後は2MBまでの画像しかアップロードすることができません。この最大値を上げるための設定を行います。

.user.iniの追加

Herokuにはphp.iniの設定を書き換える方法として.user.iniの作成を挙げています。
参考:Customizing Web Server and Runtime Settings for PHP

具体的には、まず下記のようなファイルを作成します。

.user.ini
post_max_size = 100M
upload_max_filesize = 100M

そしてこのファイルをWordpressのルートディレクトリに配置します。後はアプリをデプロイするとHerokuが自動で設定値を認識し、画像のアップロードサイズが増加しているはずです。

6. まとめ

以上がVue + Wordpressを用いてポートフォリオを作成した手順となります。初めてVueを用いてWebサイトを作成しましたが、APIの併用もしたことで大変勉強になりました。簡単にはなってしまいますが、以上で記事を締めさせていただこうと思います。最後に、参考にさせていただいた記事の執筆者の方々、本当にありがとうございます。そして勝手に引用させていただき恐縮です。

備考

今回紹介させていただいたポートフォリオは無料で作成することが可能となっております。しかしSSLを常用化したり、独自ドメインを設定したい場合は有料となってしまいます。

参考させていただいたリンクまとめ

Wordpress

Vue.js

その他

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

動かして理解するasync/await

JavaScriptのasync/awaitを動かしながら理解したい人向けです。
Node.jsで実行できます。

async/await
test()

// awaitを使う時はasyncが必要
async function test() {
    try {
        const code = await f(true)
        console.log(code)
        await f(false)
    } catch (err) {
        console.log(err)
    }
}

function f(ok) {
    // awaitする関数はPromiseを返す
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (ok) {
                // 正常終了
                resolve(200)
            } else {
                // 異常終了
                reject(500)
            }
        }, 3000)
    })
}

結果

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

?【Nuxt.js】Nuxt.jsプロジェクトをnpm run devしたらエラーが出て動かないときの解決法

環境

Windows 10
npm 6.14.6
Nuxt.js v2.14.6

やりたいこと

Nuxt.jsプロジェクトをpullしてきてVSCodeのターミナルでnpm run devしたら
いきなり大量のエラーが吐かれて実行できない、、、のでこれを解消する

PS C:\hoge\{プロジェクト名}> npm run dev

> {プロジェクト名}@1.0.0 dev C:\hoge\{プロジェクト名}
> nuxt

操作可能なプログラムまたはバッチ ファイルとして認識されていません。
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! {プロジェクト名}@1.0.0 dev: `nuxt`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the {your_project}@1.0.0 dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm WARN Local package.json exists, but node_modules missing, did you mean to install?

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\{Windowsアカウント名}AppData\Roaming\npm-cache\_logs\2020-10-10T08_18_23_229Z-debug.log

やったこと

いきなりエラーが大量に出てきて怖いですが大丈夫です、ただ必要な物がインストールされていないだけです
慌てず、急がず、とにかくnpm installを打ちます

PS C:\hoge\{プロジェクト名}> npm install

色々インストールされる
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                                     :-:
                                   .==-+:
                                  .==. :+- .-=-
                                 .==.   :==++-+=.
                                :==.     -**: :+=.
                               :+-      :*+++. .++.
                              :+-      -*= .++: .=+.
                             -+:      =*-   .+*: .=+:        
                            -+:     .=*-     .=*-  =+:       
                          .==:     .+*:        -*-  -+-      
                         .=+:.....:+*-.........:=*=..=*-     
                         .-=------=++============++====:     

                          Thanks for installing nuxtjs 
                 Please consider donating to our open collective
                        to help us maintain this package.

                           Number of contributors: 229
                              Number of backers: 377
                            Annual budget: US$ 67,637
                           Current balance: US$ 38,286

これが出てきたらNuxtインストール完了
あとは心置きなくnpm run devしましょう

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

jQueryを用いたその場編集機能の実装

実現したいこと

編集ボタンを配置し、編集ボタンを押すと要素がテキストに切り替わり登録可能になるようにしたい。

ソースコード

index.html
 <div>
   <form action""  id="food_form" method="post">
     <span id="food">焼肉</span>
     <input type="text" id="text" value="" style="display:none;">
     <input type="button" id="edit" value="編集">
     <input type="button" id="regist" value="登録" style="display:none;">
   </form>                   
 </div>



<script>
$(document).ready(function() {
    $(".edit").click(function(){
     $("#text").removeAttr('style');
       $(this).hide();
       $("#regist").removeAttr('style');
    });

    $(".add_btn").click(function(){
       $('#food_form').submit();
    });
});

</script>

自分で試したこと

現在は編集ボタンを押すと編集ボタンが消え、display:noneで隠していたtextフォームと登録ボタンが現れる仕組みでの実装を考えています。

しかしコードがすっきりしないので、textフォームや登録ボタンもJQueryで追加するやり方の方が良いのかもしくは別のやり方があるのかを知りたいです。
よろしくお願いします。

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

【Javascript】ハンバーガーメニュー作成(左、上、右から表示)

目標

なるべく無駄な記述を減らし、シンプルにしてみました。
左から.gif

開発環境

ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina

前提

※ ▶◯◯ を選択すると、説明等が出てきますので、
  よくわからない場合の参考にしていただければと思います。

実際のコード(左から、ベース)

まずは目標の左から出てくるようにします。

html
<nav id="nav">
  <ul>
    <li><a href="#">リンク1</a></li>
    <li><a href="#">リンク2</a></li>
    <li><a href="#">リンク3</a></li>
  </ul>
</nav>
<div id="hamburger">
  <span class="inner_line" id="line1"></span>
  <span class="inner_line" id="line2"></span>
  <span class="inner_line" id="line3"></span>
</div>

<style>
body{
   background-color: rgba(0,0,0,0.2);
 }
#nav{
  position: absolute;
  height: 100vh;
  width: 40%;
  left: -40%;
  top: 0;
  background: #ffffff;
  transition: .7s;
}
#nav ul{
  padding-top: 80px;
}
#nav ul li{
  list-style-type: none;
}
#hamburger {
  display: none;
  position: absolute;
  top: 20px;
  left: 30px;
  width: 50px;
  height: 44px;
  transition: 1s;
}
.inner_line {
  display: block;
  position: absolute;
  left: 0;
  width: 50px;
  height: 3px;
  background-color: #000000;
  transition: 1s;
  border-radius: 4px;
}
#line1 {
  top: 0;
}
#line2 {
  top: 20px;
}
#line3 {
  bottom: 0px;
}

.in{
  transform: translateX(100%);
}
.line_1,.line_2,.line_3{
  background: #000000;
}
.line_1 {
  transform: translateY(20px) rotate(-45deg);
  top: 0;
}
.line_2 {
  opacity: 0;
}
.line_3 {
  transform: translateY(-20px) rotate(45deg);
  bottom: 0;
}

@media (max-width: 1200px) {
  #hamburger {
    display: block;
  }
}
</style>

<script>
  function hamburger(){
    document.getElementById('line1').classList.toggle('line_1');
    document.getElementById('line2').classList.toggle('line_2');
    document.getElementById('line3').classList.toggle('line_3');
    document.getElementById('nav').classList.toggle('in');
  };
  document.getElementById('hamburger').addEventListener('click',function(){
    hamburger();
  });
</script>

補足【@media (max-width: 1200px)】
レスポンシブ対応をさせる場合は
<meta name="viewport" content="width=device-width,initial-scale=1.0">
この記述を<head></head>内部に記述してください

実際のコード(上から)

cssのみ編集します。
※ベースからの変更箇所のみ
上から.gif

#nav{
  position: absolute;
  height: 300px;
  width: 40%;
  left: 0; /* 変更 */
  top: -300px; /* 変更 */
  background: #ffffff;
  transition: .7s;
}
.in{
  transform: translateY(100%); /* 変更 */
}

実際のコード(右から)

cssのみ編集します。
※ベースからの変更箇所のみ
右から.gif

#nav{
  position: absolute;
  height: 300px;
  width: 40%;
  right: -40%; /* 変更 */
  top: 0;
  background: #ffffff;
  transition: .7s;
}
#hamburger {
  display: none;
  position: absolute;
  top: 20px;
  right: 30px; /* 変更 */
  width: 50px;
  height: 44px;
  transition: 1s;
}
.in{
  transform: translateX(-100%); /* 変更 */
}

まとめ

考え方としては、メニューの一覧を画面外に表示させておき、
クリックすると表れる仕組みです。
●css
ハンバーガーメニューの横線はinner_lineで、
クリックしたときの挙動はtransform: translate〜;で表現しています。

●javascript
クリックした時に付与するクラスを指定し、新しいcssを適用させています。

ハンバーガーメニューに関しては様々な方法があるため、興味のある方は調べてみると面白いです。

またtwitterではQiitaにはアップしていない技術や考え方もアップしていますので、
よければフォローして頂けると嬉しいです。
詳しくはこちら https://twitter.com/japwork

参考

https://codepen.io/asukas/pen/oNbxpVy

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

【Firestore × JavaScript】配列フィールドに変数で指定した複数要素を追加・削除したい

こんにちは!

今回は「Firestoreで配列に要素を複数追加・削除」する方法について、JavaScriptを使った書き方を共有したいと思います。

公式ドキュメントの例だと一つの要素を追加(削除)する例しか見当たらなかったので、記事にしました。

はじめに

以降の例では、以下の構成でusersフィールドにデータを追加する場合で考えていきます。
roomコレクション>room1ドキュメント>usersフィールド(配列)

(おさらい)配列フィールドに要素を追加する

配列に要素を追加したい場合、arrayUnion、arrayRemoveを使います。

追加

const db = firebase.firestore();
db.collection('users').doc('room1')
  .update({
    users: firebase.firestore.FieldValue.arrayUnion('user1'), // あusersフィールド(配列)に要素'user1'を追加
  });

削除

const db = firebase.firestore();
db.collection('users').doc('room1')
  .update({
    users: firebase.firestore.FieldValue. arrayRemove('user1'), // usersフィールド(配列)から要素'user1'を削除
  });

複数要素を追加したい場合(基本形)

,(カンマ)を使うことで、複数の要素を追加することができます。

追加

const db = firebase.firestore();
db.collection('users').doc('room1')
  .update({
    users: firebase.firestore.FieldValue.arrayUnion('user1', 'user2'), // usersフィールド(配列)に要素'user1','user2'を追加
  });

削除

const db = firebase.firestore();
db.collection('users').doc('room1')
  .update({
    users: firebase.firestore.FieldValue. arrayRemove('user1', 'user2'), // usersフィールド(配列)から要素'user1','user2'を削除
  });

複数要素を追加したい場合(変数を使いたい)

スプレッド構文...arrayを使うことで、複数の要素を追加することができます。

追加

const db = firebase.firestore();

const usersArr = ['user1', 'user2'];

db.collection('users').doc('room1')
  .update({
    users: firebase.firestore.FieldValue.arrayUnion(...usersArr), // スプレッド構文を使って、usersフィールド(配列)に`usersArr`の要素を追加
  });

削除

const db = firebase.firestore();

const usersArr = ['user1', 'user2'];

db.collection('users').doc('room1')
  .update({
    users: firebase.firestore.FieldValue. arrayRemove(...usersArr), // スプレッド構文を使って、usersフィールド(配列)から`usersArr`の要素を削除
  });

さいごに

配列の要素追加・削除はarrayUnionarrayRemoveが便利です!

配列取得(get)→配列加工→更新(update)のように、排他制御的のためのトランザクションが必要なくなるので、積極的に使ってみてください。

参考

https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja

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

javascriptで配列の中身に数値を入れたいときNaNになってしまう場合

配列にキーを指定して数値を入れたいとき初期化しないとNaNになる。
変数を0で初期化しないとNaNになるときがあって、それはすぐわかったが、今回の配列の中だとできなくてハマった。
ぐぐってもわからず、時間がかかってしまった。
どういう場面で、使うか不明だけど、既存のシステムがあって、一部カスタマイズしたいときなど使うことがあるかもしれない

let arrayA = [0, 1, 2, 3, 4]
let arrayB = []
for ( value in arrayA ) {   
    arrayB[0] += Number(value)
}
// NaN

一度キーを指定して0で初期化しておく。

let arrayA = [0, 1, 2, 3, 4]
let arrayB = []
for ( value in arrayA ) {   
    if (! arrayB[0]) {
        arrayB[0] = 0
    }

    arrayB[0] += Number(value)
}
// 10
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【IE限定】JavaScriptからCOMを呼び出す

IEからCOMを呼び出す方法です。
最近やったので、ノウハウをまとめます。
IE離れが進む中で今更こんなことする機会は滅多にないと思いますが、やるとなるとレガシー過ぎて情報が少なく、苦労すると思います。

開発環境

開発環境は以下の通りです。

  • Windows10
  • Visual Studio 2019
  • IE11
  • C++

インストール

Visual Studio 2019をインストールします。
今回は個人の勉強用として、Visual Studio Community 2019をインストールしました。
基本、インストーラに従って、デフォルトのまま、インストールすればよいですが、「ワークロード」で「C++ によるデスクトップ開発」を選んでください。

install_1.png

プロジェクト作成

Visual Studio Community 2019のインストールが終わったら、プロジェクトを作成します。
Windowsの検索窓で「visual」と入力すると、Visual Studio 2019が候補に表示されます。
右クリックでメニュー表示し、「管理者として実行」を選択してください。

make_0.png

Visual Studioが起動したら、「新しいプロジェクトの作成」からプロジェクトを作成します。
「新しいプロジェクトの作成」画面が表示されるので、検索欄に「atl」と入力して、「ATL プロジェクト」を検索します。
「ATL プロジェクト」が表示されたら、選択して、次に進みます。

make_1.png

あとは、デフォルトのままプロジェクト作成を進めてよいですが、「アプリケーションの種類」が「ダイナミック リンク ライブラリ (.dll)」であることは、一応、確認してください。

make_2.png

実装

プロジェクトが作成できたら、実装していきます。

COM側の実装

シンプルオブジェクトの作成

まずは、シンプルオブジェクトを追加します。
シンプルオブジェクトを追加すると、実装に必要なファイルが生成されます。
Visual Studioの「ソリューション エクスプローラー」から、プロジェクトを選択します。
右クリックでメニューを表示し、「追加」→「新しい項目」を選択します。

dev1.png

「新しい項目の追加」画面が表示されます。
「インストール済み」から「ATL」を選択し、「ATL シンプル オブジェクト」を選択します。
「追加」ボタンをクリックして次に進みます。

dev2.png

「ATL シンプル オブジェクト」画面が表示されます。
自動生成されるファイルの名前が表示されます。
今回は名前を変更せずに進めたいと思います。「完了」ボタンをクリックします。

dev3.png

これで実装に必要なファイルが作成されます。

ファイルに実装を記述

作成されたファイルに実装を記述していきます。
記述が必要なファイルは、以下3つです。

ファイル名 記述内容
IDLファイル ATLProject1.idl 公開するインターフェイス定義
ヘッダーファイル ATLSimpleObject.h C++ヘッダーファイル
実装ファイル ATLSimpleObject.cpp C++実装ファイル

IDLファイルを記述

まず、IDLファイルを記述していきます。
プロジェクト名.idl(例:ATLProject1.idl)を開きます。
interface IATLXxxxxXxxxx(例:interface IATLSimpleObject)のブロックにプロパティとメソッドを記述します。

IDLファイル
// ATLProject1.idl : ATLProject1 の IDL ソース
//

// このファイルは、タイプ ライブラリ ([!output SAFE_IDL_NAME].tlb) およびマーシャリング コードを
// タイプ ライブラリ (ATLProject1.tlb) とマーシャリング コードを生成します。

import "oaidl.idl";
import "ocidl.idl";

[
    object,
    uuid(d0b8ec50-7953-4607-86c6-6f4b2499db6f),
    dual,
    nonextensible,
    pointer_default(unique)
]
interface IATLSimpleObject : IDispatch
{
    // ★★★★★★★★★★ ここから ★★★★★★★★★★
    [propget, id(1)] HRESULT Prop1([out, retval] LONG* pVal);
    [propput, id(1)] HRESULT Prop1([in] LONG newVal);
    [propget, id(2)] HRESULT Prop2([out, retval] LONG* pVal);
    [propput, id(2)] HRESULT Prop2([in] LONG newVal);
    [propget, id(3)] HRESULT Result([out, retval] LONG* pVal);
    [propput, id(3)] HRESULT Result([in] LONG newVal);

    [id(4)] HRESULT CalcByArgs([in] LONG arg1, [in] LONG arg2, [out, retval] LONG* result);
    [id(5)] HRESULT CalcByProp();
    // ★★★★★★★★★★ ここまで ★★★★★★★★★★
};
[
    uuid(8cb35385-6c0f-4d8c-aef3-864ff7ec2143),
    version(1.0),
]
library ATLProject1Lib
{
    importlib("stdole2.tlb");
    [
        uuid(da1a207a-5427-49b2-b2fb-08b9f5fef902)
    ]
    coclass ATLSimpleObject
    {
        [default] interface IATLSimpleObject;
    };
};

import "shobjidl.idl";
プロパティについて

メンバ変数に外部から値を取得・設定するためのアクセサです。
propgetpropputが、値取得・設定のセットで、idはそのセットで同じidを振ってください。
値取得のシンタックスは以下で、コピって、IDプロパティ名型名を変更すれば、量産できます。

値取得
[propget, id(ID)] HRESULT プロパティ名([out, retval] 型名* pVal);

値設定のシンタックスは以下で、コピって、IDプロパティ名型名を変更すれば、量産できます。

値設定
[propput, id(ID)] HRESULT プロパティ名([in] 型名 newVal);
メソッドについて

何かしらの処理をする関数です。
メソッドのシンタックスは以下です。
[in]が引数、[out, retval]が戻り値になります。

メソッド
[id(ID)] HRESULT メソッド名([in] 型名 引数名, [in] 型名 引数名, ・・・, [out, retval] 型名* 戻り値名);
idについて

idは重複しない任意の値を振ります。

型について

基本的な型として、例えば、以下が使えます。

  • BSTR
  • BYTE
  • CHAR
  • DATE
  • DOUBLE
  • FLOAT
  • LONG
  • SHORT
  • VARIANT

他にもいろいろ使えるかもしれません。
構造体のような独自定義型も使えるかもしれませんが、試してません。

ヘッダーファイルを記述

IDLファイルを記述したら、ヘッダーファイルを記述します。
拡張子.hのファイル(例:ATLSimpleObject.h)を開きます。
public:にプロパティとメソッド、private:にプロパティのメンバ変数を記述します。

ヘッダーファイル
// ATLSimpleObject.h : CATLSimpleObject の宣言

#pragma once
#include "resource.h"       // メイン シンボル



#include "ATLProject1_i.h"



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "DCOM の完全サポートを含んでいない Windows Mobile プラットフォームのような Windows CE プラットフォームでは、単一スレッド COM オブジェクトは正しくサポートされていません。ATL が単一スレッド COM オブジェクトの作成をサポートすること、およびその単一スレッド COM オブジェクトの実装の使用を許可することを強制するには、_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA を定義してください。ご使用の rgs ファイルのスレッド モデルは 'Free' に設定されており、DCOM Windows CE 以外のプラットフォームでサポートされる唯一のスレッド モデルと設定されていました。"
#endif

using namespace ATL;


// CATLSimpleObject

class ATL_NO_VTABLE CATLSimpleObject :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CATLSimpleObject, &CLSID_ATLSimpleObject>,
    public IDispatchImpl<IATLSimpleObject, &IID_IATLSimpleObject, &LIBID_ATLProject1Lib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
    CATLSimpleObject()
    {
    }

DECLARE_REGISTRY_RESOURCEID(106)


BEGIN_COM_MAP(CATLSimpleObject)
    COM_INTERFACE_ENTRY(IATLSimpleObject)
    COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

// ★★★★★★★★★★ ここから ★★★★★★★★★★
private:
    LONG mProp1;
    LONG mProp2;
    LONG mResult;

public:
    STDMETHOD(get_Prop1)(LONG* pVal);
    STDMETHOD(put_Prop1)(LONG newVal);
    STDMETHOD(get_Prop2)(LONG* pVal);
    STDMETHOD(put_Prop2)(LONG newVal);
    STDMETHOD(get_Result)(LONG* pVal);
    STDMETHOD(put_Result)(LONG newVal);

    STDMETHOD(CalcByArgs)(LONG arg1, LONG arg2, LONG* result);
    STDMETHOD(CalcByProp)();
};
// ★★★★★★★★★★ ここまで ★★★★★★★★★★

OBJECT_ENTRY_AUTO(__uuidof(ATLSimpleObject), CATLSimpleObject)

実装ファイルを記述

ヘッダーファイルを記述したら、実装ファイルを記述します。
拡張子.c++(例:ATLSimpleObject.cpp)のファイルを開きます。
以下のように記述していきます。

実装ファイル
// ATLSimpleObject.cpp : CATLSimpleObject の実装

#include "pch.h"
#include "ATLSimpleObject.h"


// CATLSimpleObject


// ★★★★★★★★★★ ここから ★★★★★★★★★★
STDMETHODIMP CATLSimpleObject::CalcByArgs(LONG arg1, LONG arg2, LONG* result)
{
    *result = arg1 + arg2;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::CalcByProp()
{
    mResult = mProp1 + mProp2;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::get_Prop1(LONG* pVal)
{
    *pVal = mProp1;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::put_Prop1(LONG newVal)
{
    mProp1 = newVal;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::get_Prop2(LONG* pVal)
{
    *pVal = mProp2;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::put_Prop2(LONG newVal)
{
    mProp2 = newVal;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::get_Result(LONG* pVal)
{
    *pVal = mResult;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::put_Result(LONG newVal)
{
    mResult = newVal;
    return S_OK;
}
// ★★★★★★★★★★ ここまで ★★★★★★★★★★

ビルド

COM側を実装したら、ビルドします。
ビルド前に以下を確認してください。

  • 「x86」であること
    IEからCOMを呼び出すためか、x64では呼び出せないようです。
  • Visual Studioを管理者モードで起動していること
build.png

「ソリューション エクスプローラー」でプロジェクトを選択し、右クリックでメニューを開き、「リビルド」を選択します。

dev4.png

ビルドが終了すると、DLLがCOMに登録されています。

JavaScript側の実装

COMの実装ができたら、JavaScript側の実装を記述します。
以下の内容のHTMLファイルを作成します。

HTMLファイル
<!doctype html>

<html>

<head>
  <meta charset="utf-8">
  <title>ActiveX</title>
</head>
<body>
  <object id="activeXObj" classid="clsid:Your UUID"></object>
  <script>
    var activeXObj = document.getElementById('activeXObj');

    var result = activeXObj.CalcByArgs(1, 2);
    alert('CalcByArgs result: ' + result);

    activeXObj.Prop1 = 100;
    activeXObj.Prop2 = 200;
    activeXObj.CalcByProp();
    alert('CalcByProp result: ' + activeXObj.Result);
  </script>
</body>
</html>

Your UUIDには、IDLファイルにあるUUIDを記述します。
IDLファイル内にUUIDが複数あるので、注意してください。
(IDLファイルを記述を参照)
以下のUUIDを使います。

IDLファイル
library ATLProject1Lib
{
    importlib("stdole2.tlb");
    [
        // ★★★★★★ これ ★★★★★
        uuid(da1a207a-5427-49b2-b2fb-08b9f5fef902)
    ]
    coclass ATLSimpleObject
    {
        [default] interface IATLSimpleObject;
    };
};

上記の例では、HTMLの指定は以下になります。

<object id="activeXObj" classid="clsid:da1a207a-5427-49b2-b2fb-08b9f5fef902"></object>

動作確認

実装が終わったら、動作確認します。
作成したHTMLファイルをIEで開きます。
IEで開くと、以下のメッセージが表示されます。
「ブロックされているコンテンツを許可」をクリックします。

dev5.png

次に以下のメッセージが表示されます。
「はい」をクリックします。

dev6.png

JavaScriptからCOMにアクセスが行われ、結果がアラートダイアログで表示されます。

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

再帰関数を使って数値文字列(負数、小数含む)をカンマ区切りにする処理

練習でアプリを作成しているときに作ったコード。
toLocaleString()や正規表現は使わずに、再帰関数を使って数値文字列のカンマ区切りを実装しています。

コード

separate_commas.js
/**
 * 数値文字列の整数部分のみを3桁ごとにカンマで区切る
 * @param  {string} str 数値文字列。
 * @returns {string} 3桁ごとにカンマで区切った数値文字列
 */
const separateNumberByCommas = (str) => {
  const array = str.replace("-", "").split(".");
  const result = array.length > 1
    ? separateIntegerByCommas(array[0]) + "." + array[1]
    : separateIntegerByCommas(array[0]);
  return str.includes("-") ? "-" + result : result;
};

/**
 * 整数文字列を3桁ごとにカンマで区切る
 * @param  {string} str 整数文字列。
 * @returns {string} 3桁ごとにカンマで区切った整数文字列
 */
const separateIntegerByCommas = (str) => {
  const len = str.length;
  return len > 3
    ? separateIntegerByCommas(str.slice(0, len - 3)) + "," + str.slice(len - 3)
    : str;
};

実行例

sample.js
console.log(separateNumberByCommas("123"));       //123
console.log(separateNumberByCommas("1234"));      //1,234
console.log(separateNumberByCommas("1234."));     //1,234.
console.log(separateNumberByCommas("1234.0"));    //1,234.0
console.log(separateNumberByCommas("1234.5678")); //1,234.5678
console.log(separateNumberByCommas("-123456.0")); //-123,456.0

separateNumberByCommas()で小数点以下とマイナス記号を一旦省き、separateIntegerByCommas()で再帰的にカンマで区切っています。
再帰が終わったら、省いていた小数点とマイナス記号を再度結合します。

8行目のarray.length > 1は小数点を省いたかどうかを判定しています。
小数点があった場合は7行目で要素数2の配列が作成されるためtrueに、
もともと小数点が無い場合は7行目で要素数1の配列が作成されるため8行目でfalseになります。
末尾が小数点(例:123.)の場合は空文字が入った要素が作成されるためtrueになります。
array.length > 1array[1]に変えれば末尾の小数点も省けます)

toLocaleString()や正規表現を使わなかった理由

  • 0.とか0.0など末尾が小数点だったり小数点以下に0しかない文字も表示できるようにしたいけど、toLocaleString()だとこのような文字は小数点以下が省略されてしまう。
  • 正規表現使えば数行で実装できるらしい、けど正規表現良く分からないし理解できてないものをコピペして使うのは避けたい。
  • for文でもいいけど再帰関数の方が三項演算子使ってスマートに書ける。
    あとlet使わないで済む。

参考

数値をカンマ区切りにする | Qiita

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

Vue.js(TypeScript)でvue-click-outsideライブラリを使う

Vue.js(TypeScript)で@types/vue-click-outsideが存在せず、インポートに苦戦したのでメモ書き程度に残しておきます。

結論

require('vue-click-outside')で読み込んで、directivesに記載

Compornent.vue
<template>
  <div>
    <button v-click-outside="click">クリック</button>
  </div>
</template>

<script lang="ts">
import { Component, Vue} from 'nuxt-property-decorator'
const ClickOutside = require('vue-click-outside')

@Component({
  directives: {ClickOutside}
})

export default class Compornent extends Vue {
  click() {
    alert('click');
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者がReact & Firebaseを使って収支管理アプリを作成(解説編)

今回作ったもの

ログイン機能付きの収支管理アプリです。
いわゆる家計簿アプリ的な物です。
毎月の収入と支出をリスト化し月の残高を表示します。

【操作動画】 
*GIFなので画質悪くてすいません。
GIF.gif

感想編については、別途記事を書いてるので、ご興味ございましたら是非ご覧下さいませ!
初心者がReact学習歴1週間でWebアプリ作成に挑戦してみた

コードについて

長くなってしまうので全部は載せておりません。
必要だと思うところだけ解説してます。
スタイルを基本CSSで装飾してますが、classNameは邪魔だと思うのでこの記事では消してます。
全コードはGithubに載せてます。
こちら

使用技術

  • React(version 16.13.1)
    • Router
    • クラスの代わりにHooks (useState, useEffect, useContext)
  • Firebase
    • Authentication
    • Cloud Firestore
    • Hosting

Reactの準備

create react appで作成

npx create-react-app my-app
cd my-app
npm start

Firebaseの準備

プロジェクトを作成

スクリーンショット 2020-10-05 午後3.47.11.png

作成方法は手順に従えば大丈夫です。
簡単なので割愛しますが、全体の流れはこちらのYoutubeを参考にしました。
日本一わかりやすいReact入門【実践編】#3...Firebaseプロジェクトの作成と初めてのデプロイ

注意点として、node.jsとfirebaseのバージョンにより少し挙動が動画内容と異なります。

ポイントは、

  • ロケーション設定は初めにやる。東京なら「asia-northeast1」
  • プランは現在の安定node versionだとSparkではなくBlazeプラン(従量制)になります
  • Blazeプランなので、念の為Google Cloud Platformを作成して、アクセス& 料金状況/アラーム通知を受信できるよう設定します(任意)

Authenticationの設定

Sign-in methodよりステータスを有効にする
今回は「メール/パスワード」を使用
signinmethod.png

Cloud Firestoreでデータベース作成

  • テストモードで実行
  • とりあえずコレクション/ドキュメントを追加してみる(イメージのため)

最終的なコレクション/ドキュメント構成はこちらです。

スクリーンショット 2020-10-05 午後3.50.32.png

Firebaseの設定

Firebaseの情報をwebアプリに登録します。
プロジェクト内容は一応セキュリティを考慮し.envに登録します。
.envgitignoreすれば公開される心配がないということですね。

REACT_APP_FIREBASE_KEY="APIキー"
REACT_APP_FIREBASE_DOMAIN="プロジェクト.firebaseapp.com"
REACT_APP_FIREBASE_DATABASE="https://プロジェクト.firebaseio.com"
REACT_APP_FIREBASE_PROJECT_ID="プロジェクト"
REACT_APP_FIREBASE_STORAGE_BUCKET="プロジェクト.appspot.com"
REACT_APP_FIREBASE_SENDER_ID="ID番号"

srcディレクトリ直下にfirebaseディレクトリとFirebase.jsファイルを作成します。
ここでFirebaseの初期化処理が行われます。
authdbも作り、毎回全部書かなくて済むようにします。

src/firebase/Firebase.js
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";

firebase.initializeApp({
  apiKey: process.env.REACT_APP_FIREBASE_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_DOMAIN,
  databaseURL: process.env.REACT_APP_FIREBASE_DATABASE,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID
});

const auth = firebase.auth();
const db = firebase.firestore();

export { auth, db }

ログイン機能の実装

authディレクトリを作成し、こちらに認証機能系は集約させます。

ディレクトリ構成

src
 ├── auth
    ├── AuthProvider.js
    └── Login.js
    └── PrivateRoute.js
    └── SignUp.js
 ├── components
 ├── firebase
 ├── App.js

まず、App.jsにログイン状態で表示ページを変える為、Routerを作ります。
exactはpathが「含む」とならないように指定。

src/App.js
const App = () => {
  return (
    <AuthProvider>
      <Router>
        <Switch>
          <PrivateRoute exact path="/" component={Home} />
          <Route exact path="/login" component={Login} />
          <Route exact path="/signup" component={SignUp} />
        </Switch>
      </Router>
    </AuthProvider>
  );
};

export default App;

AuthPrivider.js

認証の情報(ユーザーがログイン、サインアップする)は、こちらで作ります。
そしてユーザー情報が必要なコンポーネントでuseContextを使います。
通常データはトップダウン形式でpropsを渡さないといけないですが、
contextを使うことで、コンポーネントツリーに簡単にデータを共有することができます。

useContextについてこちらの記事が非常にわかりやすかったです。
useContextの使い方
こんなに簡単なの?React useContextって

src/auth/AuthProvider.js
import React, { useEffect, useState } from "react";
import { auth } from "../firebase/Firebase";

const AuthContext = React.createContext() 

const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);

  //サインアップ後認証情報を更新
  const signup = async (email, password, history) => {
    try { 
      await auth.createUserWithEmailAndPassword(email, password);
      auth.onAuthStateChanged(user => setCurrentUser(user));
      history.push("/");
    } catch (error) {
      alert(error);
    }
  };

  //ログインさせる
  const login = async (email, password, history) => {
    try {
      await auth.signInWithEmailAndPassword(email,password);
      auth.onAuthStateChanged(user => setCurrentUser(user));
      history.push("/");
    } catch (error) {
      alert(error);
    }
  }

  //初回アクセス時に認証済みかチェック
  useEffect(() => {
    auth.onAuthStateChanged(setCurrentUser);
  }, []);

  return (
    <AuthContext.Provider value={{ signup, login, currentUser}}>
      {children}
    </AuthContext.Provider>
  )
}

export {AuthContext, AuthProvider}

初期値のユーザーのステートはnullですね。

signup関数
引数にemailpasswordhistoryを渡して非同期処理を行います。
auth.createUserWithEmailAndPassword(email, password)は、
firebaseのメソッドでemailpasswordを元にアカウントが作成されます。
その後userの情報を取得し、CurrentUserにセットします。

history.push("/")は、reactRouterの画面遷移させる機能です。
今回はログイン後、Home画面に行きます。

login関数
同じ様に、ユーザーがログインしたら情報を取得しCurrentUserを更新するようにします。
auth.signInWithEmailAndPassword(email,password)
これもまたfirebaseのメソッドです。

あとは、最初にログインしてるか確認する為、useEffectで一回だけauth.onAuthStateChangedを実行します。
※一回だけ実行したいので、第二引数には空の配列[]を渡します。

PrivateRoute.js

アプリのメイン画面Home.jsは、PrivateRouteに指定します。
ここで、ユーザーがログインしてれば→メイン画面を表示。
未ログインの場合→ログイン画面を表示。の作業を行なってます。

src/auth/PraveteRoute.js
import React, { useContext } from "react";
import { Route } from "react-router-dom";
import { AuthContext } from "./AuthProvider";
import Login from "./Login";

const PrivateRoute = ({ component, ...rest}) => {
  const { currentUser } = useContext(AuthContext);
  //AuthContextからcurrentUserを受け取る

  const Component = currentUser ? component : Login;
  //currentUserがtrueの場合component=Home、falseならLoginコンポーネントにroute

  return <Route {...rest} component={Component} />;
};

export default PrivateRoute;

...restは、残りのpropsをまとめて引数に渡してます(今回他のpropsはないですが)
この...ですが、以前RestParametersの記事を書きましたのでよろしければご参照ください。
ES6の新しい構文です!
スプレッド構文とRestパラメータを理解する

SignUp.jsとLogin.js

SignUp.jsコンポーネントでは、ユーザー登録画面の表示、登録内容を取得します。
handleSubmitが実行される時、入力されたemailpasswordの内容をAuthProviderで作ったsignup関数の引数に渡してデータが登録されます。
アップデート後のhistory(情報)を渡すために、最後exportの時withRouter(SignUp)を使っています。

src/auth/SignUp.js
const SignUp = ({ history }) => {
  const { signup } = useContext(AuthContext);
  //AuthContextからsignup関数を受け取る

  const handleSubmit = event => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    signup(email.value, password.value, history);
  };

  return (
      <div>
        <h1>Sign Up</h1>
        <form onSubmit={handleSubmit}>
          <div>
            <label>E-mail Address</label>
            <input name="email" type="email" placeholder="email@gmail.com" />
          </div>
          <div>
            <label>Password</label>
            <input name="password" type="password" placeholder="Password"/>
          </div>
            <SignUpButton type="submit">SIGN UP</SignUpButton>
        </form>
        <Link to="/login">SignInへ戻る</Link>
      </div>
    </div>
  );
};

export default withRouter(SignUp);

Login.jsも似たような感じで作ります。
signup部分をloginに変えるだけですね。

src/auth/Login.js
const Login = ({ history }) => {
  const { login } = useContext(AuthContext);
  //AuthContextからlogin関数を受け取る

  const handleSubmit = event => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    login(email.value, password.value, history);
  };

コンポーネント・ファイル構成

続いてメイン画面です。
アプリのメイン画面を担うコンポーネント達がこちら。
メイン(親)のコンポーネントは、Home.jsになります。

src
 ├── auth
 ├── components
    ├── Home.js
    └── Header.js
    └── Balance.js
    └── IncomeExpense.js
    └── AddItem.js
    └── ItemsLists.js
    └── IncomeItem.js
    └── ExpenseItems.js
    └── TotalIncomeExpense.js //共通関数ファイル
 ├── firebase
 ├── App.js

画面上ではこんな感じ。
main.png

ステートの作成/更新

親コンポーネントから子コンポーネントに渡す為、ステートは全てHome.jsで作成します。

作成したステート達

src/compoments/Home.js
  const [inputText, setInputText] = useState("");
  const [inputAmount, setInputAmount] = useState(0);
  const [incomeItems, setIncomeItems] = useState([]);
  const [expenseItems, setExpenseItems] = useState([]);
  const [type, setType] = useState("inc")
  const [date, setDate] = useState(new Date());

収入incomeの配列incomeItemsと、支出expenseの配列expenseItemsは、後々計算が楽なので分けて作成。

データを追加する

Firestoreからデータを取得・追加は全てHome.jsで行います。

こちらは追加バージョン

src/compoments/Home.js
  const addIncome = (text, amount) => {
    const docId = Math.random().toString(32).substring(2);
    const date = firebase.firestore.Timestamp.now();
    db.collection('incomeItems').doc(docId).set({
      uid: currentUser.uid,
      text,
      amount,
      date,
    })
    .then(response => {
      setIncomeItems([
        ...incomeItems, {text: inputText, amount: inputAmount, docId: docId , date: date}
      ]); 
    })
  }
  • 収入income用の関数addIncomeを用意。引数にはユーザーが入力したtextamountを渡す。
  • docIdをこちらで作る。
  • 収入リストが順番に並べられるようにdateを作成。firebase.firestore.Timestamp.now()(入力時間が登録される)→firebaseのメソッド
  • どこのcollectionのdocumentに追加するかは、firebaseのメソッドを使用db.collection('incomeItems').doc(docId).set({})
  • setしたいデータを配列として追加uid ~ date
  • その後.thenで、reactアプリ側のsetIncomeItemsのステートを更新

ポイントは、
ユーザーが削除ボタンを押した時、データを削除するのにdocIdを使います。
reactアプリと連動させたいので、こちら側で手動で作成してます。
※その時、数値だとエラーになるので文字列に変換.toString(32).substring(2)
手動で作らない場合は、「.set」ではなく「.add」で自動生成可能。

これと同じ内容で出費expense用の関数も用意すれば、値はFirestoreとReact上で無事追加/更新されます。

データを取得する

Firestoreからデータを取ってきて、アプリ上で表示させます。

src/compoments/Home.js
  const getIncomeData = () => {
    const incomeData = db.collection('incomeItems')
    incomeData.where('uid', '==', currentUser.uid).orderBy('date').startAt(startOfMonth(date)).endAt(endOfMonth(date)).onSnapshot(query => {
      const incomeItems = []
      query.forEach(doc => incomeItems.push({...doc.data(), docId: doc.id}))
      setIncomeItems(incomeItems);
    })
  }
  • 取得したいデータのIncome用の関数getIncomeDataを作成
  • コレクションincomeItemsのドキュメントを取得→変数incomeDataに代入
  • uidが現在のユーザーと一致する場合のstartOfMonth~endOfMonthのドキュメントを取得
  • 取得したデータを保存する空の配列incomeItemsを作成
  • その配列にドキュメントのdataidpush(追加)する
  • reactアプリ側のincomeItemsの配列を更新する

ポイントは、
orderByのメソッドでdateを昇順にしてます。
リストがバラバラに表示されてしまうので、制御する為に必要です。
また、orderByで昇順にしようとするとfirebaseから「indexを作れ」というエラーが出ます。
その際、親切にURLが表示されるのでそこにアクセスしてindexを作ればOKです(結構時間かかります)
参考記事:複合index

startAtendAtは、その月の分だけ表示させる為です。
この引数の中身については、後ほど詳細を書きます。

これと同じ内容で出費expense用の関数も用意すれば、無事Firestoreからデータを取得できて、Reactのステートに更新/表示がされます。

尚、データを取得するタイミングは、useEffectを使って操作します。

  1. 最初の1回のみ実行してほしい。引数は空の配列。
  2. dateが更新されるたびに実行してほしい。※dateは次で解説してますがヘッダーの月の部分です。
src/compoments/Home.js
  useEffect (() => {
    getIncomeData();
    getExpenseData();
  }, []);

  useEffect(() => {
    getIncomeData();
    getExpenseData();
  }, [date]);

月ごとにデータを表示させる

月ごとにデータを分けて表示させる為、ステートで作ったdateを使います。
初期値は現在の日時が入ってます。

src/compoments/Home.js
const [date, setDate] = useState(new Date());

ヘッダーに渡して表示

dateは、ユーザーがヘッダーの"前月"か"次月"ボタンを押すと更新されます。
↓の関数で月の部分を変えてます。

src/compoments/Home.js
  //for Header
  const setPrevMonth = () => {
    const year = date.getFullYear();
    const month = date.getMonth()-1;
    const day = date.getDate();
    setDate(new Date(year, month, day));
  }

  const setNextMonth = () => {
    const year = date.getFullYear();
    const month = date.getMonth()+1;
    const day = date.getDate();
    setDate(new Date(year, month, day));
  }

これらはHeader.jsで使うのでpropsで渡してあげます。

src/compoments/Home.js
<Header 
  date={date}
  setPrevMonth={setPrevMonth}
  setNextMonth={setNextMonth}
/>

Header.jsでは、現在の月を表示させる為、yearmonthを作り、
隣に前月と次月ボタンを表示させます。

src/compoments/Header.js
  const today = date;
  const year = today.getFullYear();
  const month = today.getMonth()+1;

  return (
    <div className="head">
      <SignOutButton onClick={() => auth.signOut()}>Sign Out</SignOutButton>
      <div>
        <button onClick={() => setPrevMonth()}>←前月 </button>
        <h1>{year}{month}</h1>
        <button onClick={() => setNextMonth()}> 次月→</button>
      </div>
    </div>
  )

これでボタンをクリックしたら、setPrevMonth()setNextMonth()が実行され、月の表示が変わります。

ユーザーの入力内容を操作する

ユーザーが入力した内容を取得する関数は、AddItem.jsコンポーネントで行ってます。

src/components/AddItem.js
export const AddItem = ({ addIncome, addExpense, inputText, setInputText, inputAmount, setInputAmount, type, setType, selectedMonth, thisMonth}) => {

  const typeHandler = (e) => {
    setType(e.target.value);
  }

  const inputTextHandler = (e) => {
    setInputText(e.target.value);
  };

  const inputAmountHandler = (e) => {
    setInputAmount(parseInt(e.target.value));
  }

  const reset = () => {
    setInputText("");
    setInputAmount("");
  }

  const submitItemHandler = (e) => {
    e.preventDefault();
    if (inputText == '' || inputAmount == '0' || !(inputAmount > 0 && inputAmount <= 10000000)) {
      alert ('正しい内容を入力してください')
    } else if ( type === 'inc') {
      addIncome(inputText, inputAmount) 
      reset();
    } else if ( type === 'exp' ) {
      addExpense(inputText, inputAmount)
      reset();
    }
  }

  const thisMonthForm = () => {
    return (
      <form>
        <select onChange={typeHandler}>
          <option value="inc">+</option>
          <option value="exp">-</option>
        </select>
        <div>
          <label>内容</label>
          <input type="text" value={inputText} onChange={inputTextHandler}/>
        </div>
        <div>
          <label>金額</label>
          <input type="number" value={inputAmount} onChange={inputAmountHandler}/>
          <div></div>
        </div>
        <div>
        <AddButton type="submit" onClick={submitItemHandler}>追加</AddButton>
        </div>
      </form> 
    )
  }

  const otherMonthForm = () => {
    return (
      <form></form>
    )
  }

  return (
    <>
    {thisMonth === selectedMonth ? thisMonthForm() : otherMonthForm()}
    </>
  )

} 
  • 収入income、出費expense、どちらに追加するのかはtypeで分けてます。
  • ユーザーが、option を選択した時にtypeHandler関数でtypeの値を更新します。
  • inputの値とamountの値もonChangeで取得します。
  • amountについては、後に計算するのでparseIntで値を数値化します。

submitItemHandlerで追加ボタンが押された時、操作してる内容はこちら。

  • デフォルトのイベント(動作)をキャンセル
  • 正しい内容が入力されてない場合、エラーを表示
  • incタイプなら、Home.jsで定義したaddIncomeの引数に ユーザーの入力内容inputTextinputAmountを渡す
  • expタイプも同様

→Firestoreのデータが追加され、reactアプリのステートも更新されるという流れ。

ヘッダーが今月なのか、今月でない月かによって表示方法を変える為、条件付きレンダーを行ってます。
(※今月のみ追加フォームを表示させる為)

この条件に使ってるselectedMonththisMonthは、
他のコンポーネント(リスト)でも使うので、Home.jsで作ってpropsで渡してます。

src/components/Home.js
  //operate add form and income/expense list
  const selectedMonth = date.getMonth() + 1;
  const today = new Date();
  const thisMonth = today.getMonth() + 1;

リストの表示

リストの表示はItemsList.jsを作り、そこでitemsに対してmapを行い、IncomeItemExpenseItemをそれぞれ表示します。

src/components/ItemsList.js
export const ItemsList = ({ deleteIncome, deleteExpense, incomeItems, expenseItems, incomeTotal, selectedMonth, thisMonth}) => {

  return (
    <div>
      <div>
        <h3>収入一覧</h3>
          <ul>
            {incomeItems.map((incomeItem) => (
              <IncomeItem 
                deleteIncome={deleteIncome}
                incomeText={incomeItem.text}
                incomeAmount={incomeItem.amount}
                incomeItem={incomeItem}
                key={incomeItem.docId}
                selectedMonth={selectedMonth}
                thisMonth={thisMonth}
              />
            ))}
          </ul>
      </div>
      <div>
        <h3>支出一覧</h3>
        <ul>
            {expenseItems.map((expenseItem) => (
              <ExpenseItem
                deleteExpense={deleteExpense}
                expenseText={expenseItem.text}
                expenseAmount={expenseItem.amount}
                expenseItem={expenseItem}
                key={expenseItem.docId}
                incomeTotal={incomeTotal}
                selectedMonth={selectedMonth}
                thisMonth={thisMonth}
              />
            ))}
          </ul>
      </div>
    </div>
  )
}

mapを実行して作られた一つ一つの項目を表示されるコンポーネントがこちら↓

Incomeバージョン

src/components/IncomeItem.js
export const IncomeItem = ({ deleteIncome, incomeItem, incomeText, incomeAmount, thisMonth, selectedMonth}) => {

  const deleteHandler = () => {
    deleteIncome(incomeItem.docId);
  }

  const showThisMonth = () => {
    return (
      <li>
      <div>{incomeText}</div>
      <div>+{Number(incomeAmount).toLocaleString()}</div>
      <button onClick={deleteHandler}>×</button>
      </li>
    )
  }

  const showPastMonth = () => {
    return (
      <li>
      <div>{incomeText}</div>
      <div>+{Number(incomeAmount).toLocaleString()}</div>
      </li>
    )
  }

  return (
    <>
      {thisMonth === selectedMonth ? showThisMonth() : showPastMonth()}
    </>
  )
}

Number(incomeAmount).toLocaleString()は、カンマ「,」を表示させる為。
deleteHandlerは "×" を押した時にdleteIncomeを実行してます。
このdelteIncomeは、fireStoreのdocIdの関係上、Home.jsで定義されてます。↓
引数にこのincomeItem.docIdを渡せば該当のアイテムは削除されます。

src/compoments/Home.js
  const deleteIncome = (docId) => {
    db.collection('incomeItems').doc(docId).delete()
  }

firebaseのメソットを使い、incomeItemsコレクションにある、該当ドキュメントを削除してます。

あとは、スタイル上、表示の仕方を変えたいので、ここでも条件付きレンダーを行ってます。
→ showThisMonthshowPastMonthで分ける。
これで今月以外、削除ボタンを表示させないようにしてます。

これと同じようにexpenseバージョンも作ればOK

収入/支出の値を計算する

収入と支出の各合計

値の計算はそれぞれのincomeItemsexpenseItemsの配列から、amountを取り出して計算します。

src/components/IncomeExpense.js
export const IncomeExpense = ({ incomeTotal, expenseItems }) => {

  const expenseAmounts = expenseItems.map(expenseItem => expenseItem.amount);

  const expenseTotal = expenseAmounts.reduce((acc, cur) => acc += cur, 0);

  const percentage = () => {
    if (incomeTotal >= 1) {
      return `${Math.round((expenseTotal / incomeTotal) * 100)} %`;
    } else {
      return '---';
    }
  };

  return (
    <div>
      <div>
        <h2>収入</h2>
        <div>
          <p>+ {Number(incomeTotal).toLocaleString()}<span> </span></p>
      </div>
        </div>
      <div>
        <h2>支出</h2>
        <div>
          <p>- {Number(expenseTotal).toLocaleString()}<span> </span></p>
          <div>{percentage()}</div>
        </div>
      </div>
    </div>
  )
}

mapreduceについては、こちらを元に別で記事を書いています。
filterは途中色々変えたので、結局今回のアプリに使っていません。

map・filter・reduceの書き方・使い方

支出expenseの合計計算
- expenseAmountexpenseItemsの中のamountだけ取り出した配列を代入します。
- expenseAmountを使って、累計を計算し、expenseTotalとします。

これを収入incomeItemsでも同じことをします。
ただincomeバージョンについては、他のコンポーネントでも使うので、共通関数ファイルTotalIncome.jsを作成してます。

TotalIncome.jsで関数totalCalcを作り、Home.jsincomeItemsを引数に渡してます。

src/components/Home.js
  // calculate % and show total
  const incomeTotal = totalCalc(incomeItems);
src/components/TotalIncome.js
export const totalCalc = (incomeItems) => {
  const incomeAmounts = incomeItems.map(incomeItem => incomeItem.amount);
  return incomeAmounts.reduce((acc, cur) => acc += cur, 0);
};

こちらを使う他のコンポーネントとは、
割合%を表示するIncomeExpense.jsExpenseItemと、計算に必要なBalance.jsになります。

残高の計算

総合計の残高を計算をするBalance.jsコンポーネントでは、
IncomeTotal - ExpenseTotalをすれば計算できます。

src/components/Balance.js
const balance = incomeTotal - expenseTotal

アプリを公開

あとは公開するだけです!
私の場合はfirebase logininitは先に済ませてました。

> firebase login
> firebase init
- 選択項目は FireStore, Functions, Hosting
- publicディレクトリはbuildにする
> npm run build
> firebase deploy

あとがき

今回のアプリは全てデータをpropsで親から子供に渡しているので、
どうしてもメインのHome.jsが少しボリューミーになってしまいました。(そういうものなのか?)
初心者が書いたコードですので、ご理解いただければと思います。
こうした方が良い等ありましたら、是非ご指摘お願いします!
日々勉強して、もっと良い書き方でコードを書けるよう頑張ります。

参考

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

【JS学習その⑩】JavaScriptにおけるgetter/setterとstatic

JS学習シリーズの目的

このシリーズは、私ジャックが学んだJavaScriptのメカニズムについてアウトプットも兼ねて、
皆さんと知識や理解を共有するためのものです。
(理解に間違いがあればご指摘いただけると幸いです)

getter/setterとは

  • getter => 「プロパティが参照された際に行う処理
  • setter => 「プロパティに変更があった際に行う処理

プロパティにはディスクリプターというものがあり、その中にgetとsetがある

main.js
function Person1(name, age) {
    this._name = name;
    this._age = age;
}

Object.defineProperty(Person1.prototype, 'name', {
    get: function() {
        return this._name;
    },
    set: function(val) {
        this._name = val;
    }
)};

const p1 = new Person1('Bob', 23);

console.log(p1.name); /*Bob*/

p1.name = 'Tom';
console.log(p1.name); /*Tom*/

上記のコードでは、
Object.definePropertyメソッドでPerson1.prototype内のnameプロパティのディスクリプターを直接定義しています。
上記のgetがgetter,setがsetterです。

また、ES6からは上記をクラス構文で次のように簡略化して記述できます。

main.js
class Person2 {
    constructor(name, age) {
        this._name = name;
        this._age = age;
    }

    get name() {
        return this._name;
    }

    set name(val) {
        this._name = val;
    }
}

const p2 = new Person2('Bob', 23);
console.log(p2.name); /*Bob*/

p2.name = 'Tom';
console.log(p2.name); /*Tom*/

静的メソッドとは

インスタンス化せずに直接実行できるメソッド
静的メソッドはクラス内でstaticというキーワードを使用して定義します。

main.js
class Person2 {
    constructor(name, age) {
        this._name = name;
        this._age = age;
    }

    get name() {
        return this._name;
    }

    set name(val) {
        this._name = val;
    }

    static hello() {
        console.log('hello');
    }
}

Person2.hello() /*hello*/

上記のhelloメソッドが静的メソッドになります。
前述したように、静的メソッドはインスタンス化せずに直接実行できます。ただ、注意点があります。
それは、静的メソッド内(static内)ではthisは使えないということです。

また、静的メソッドは他のメソッドと違い、__prototype__内に定義されるのではなくconstructor内に生成されます
だからインスタンス化しなくても直接実行できるという訳です。

main.js
function Person1(name, age) {
    this._name = name;
    this._age = age;
}

Person1.hello = function() {
    console.log('hello');
}

Person1.hello(); /*hello*/

よってクラス内でstaticを使って定義した静的メソッドは上記のようにして書いたものと同じということです。

まとめ

いかがでしたでしょうか。
getter,setterとstatic(静的メソッド)はJavaScriptにおいて重要なのでしっかり理解しておきましょう!

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

#1【初心者】知識0からeclipseでWebアプリ(Webサイト)を作る.「 Webアプリを作る為の環境を構築しよう」

はじめに

友人とWebサイトを1から作りたいという話となり,右も左もわからない状態からeclipseを使ってとりあえず触りながらのメモ書きをしていく.(調べても知ってて当然だよねって感じで細かい設定とか書いてないサイトが多い...)

環境一覧
プログラミングソフト:Eclipse 2020
JAVA SE 14
Tomcat 8.5.88

  • 環境作成編

#1 Webアプリを作る為の環境を構築しよう(この記事)2020 10月更新

#2 GitHubとEclipseを繋げて動的Webアプリを共同開発する

  • デザインを考えよう編

  • フロントエンド作成編

  • バックエンド作成編

  • 実際にサービスを動かしてみよう編

今回の記事では,

1.Eclipseをインストールする.
2.Javaのインストール
3.パースペクティブの設定
4.プロジェクトの作成
5.サーブレットの作成
6.サーブレットの実行

という流れ.

単語メモ
パースペクティブ:各々のアプリを開発するのに最適な画面設定
サーブレット:Webサーバ上(バックエンド)で動くプログラム,今回の記事で作成するのはデバック用のローカルサーバ.

1.Eclipse をインストールする.

Eclipseには日本語化などの面倒な設定を行わずにそのまま使い始めることができる「Pleiades All in One」というエディションが用意されている.
ダウンロードサイトより,Eclips 2020をダウンロードする.

01.jpg

様々なバージョンが表示されるが,今回使用するのは Windows10 64bit の Java Full Edituonをダウンロードする.

02.jpg

勝手にダウンロードが始まる.
始まらなければ青く表示されたURLを押せばダウンロードが始まる.

03.jpg
ダウンロードした.zipファイルを解凍する.
Windowsの標準解凍ソフトだと上手く解凍できないらしいので7-Zipを使って解凍する.

04.jpg

解凍するとpleiadesというフォルダが現れるので,Windows(C:)フォルダに直接置いておく(場所は自由だが C: の直下が分かりやすい).

05.jpg

Windows(C:)直下に移動したら,C:\pleiades\eclipseフォルダ内にあるeclipse.exeを右クリックしてショートカットを作成して,デスクトップなどの分かりやすい場所に置き直しておく.(アプリとしてインストールしないためショートカットから起動する)

06.jpg

07.jpg

初回起動ではワークスペース(データの保存場所)をどこにするか聞かれるので,ドキュメントのフォルダ内にeclipseというフォルダを新規作成して,参照からフォルダを変更しておく.(デフォルトでも問題ないが分かりやすい場所に移動しておく)

08.jpg
↓ 変更する
09.jpg

変更したら起動(L)を押して画面が表示されればインストール完了.

10.jpg

2.Java SE をインストールする.

今回は JAVA SEの14を使用する.
トップページから14を選択するか,JAVA SE 14 ダウンロードサイトから Windows x64 Installer をダウンロードする(なぜかChromeだと動かない時があるのでIEを使ったほうがいい).

11.jpg

特に設定などはないのでそのままインストールするだけ.

3.パースペクティブの設定

右上のJavaアイコンの横にある①+マーク(パースペクティブ)を押すとリストが出てくるので②Java EEを選択する.

12.jpg

Java EEを開くとサーバー管理などのサポートがあるサーブレットパースペクティブ(画面構成)となる.
サーバータブなどはWebアプリを開発する際に必須なのでしっかりとJava EEに変更してから開発を開始する.

13.jpg

4.プロジェクトの新規作成

ファイル→新規→その他を選択

14.jpg

ウィザードからWebフォルダ内にある①「動的Webプロジェクト」を選び,②次へを押す.

15.jpg

設定画面が出てくるのでプロジェクト名を好きな名前で設定し,ターゲットランタイムをTomcat8 (Java8)に変更する.
変更すると動的モジュールバージョンと構成がターゲットランタイムに合わせて自動的に変更される.変更できたら次へを押す.

17.jpg

ここはデフォルトで大丈夫なので次へを押す.

18.jpg

web.xmlデプロイメント記述子の作成にチェックを入れ,完了を押すとプロジェクトが作成される.

19.jpg

これでサーブレット作成する準備が完了.

20.jpg

5.サーブレットの作成

サーブレットを作成するためにJavaリソース→srcを右クリックしてサーブレットを選択する.

21.jpg

とりあえず名前を適当に①sample_main ②sample と設定して完了を押す.

22.jpg

無事に基本的なコードが記述されたサーブレットが作成されました.

23.jpg

Webサーバのタブを開いて,「使用可能なサーバがありません.このリンクをクリックして新規サーバを作成してください.」という部分をクリックする.

24.jpg

Apacheフォルダ内から「Tomcat v8.5」サーバを選択して次へを押す.

25.jpg

サーバに追加したいリソースを①選択して,②追加を押す.

26.jpg

右側にファイルが移動しているのを確認したら完了を押す.

27.jpg

サーバーが作成されるので,右クリックして公開を押す.

28.jpg

次にもう一度右クリックして再開を押す.

29.jpg

問題がなければサーバの右側にあるステータスが[起動済み,同期済み]となる.

30.jpg

次にchromeを開いて,URLに以下を入力.

http://localhost:8080/[プロジェクト名]/[クラス名]

今回手順通りに作っていれば,

http://localhost:8080/test/sample

こんな画面が表示されれば問題なく動作している.

31.jpg

試しに

ここにある

("Served at: ")

("tesutesu")

に変えてみる(日本語で打つと文字化けするのでアルファベットで入力する).

32.jpg

しっかり更新されてますね.

33.jpg

とりあえずこれで開発環境の設定は完了,とりあえず次回はGit hubで共有してみる.

次→[]

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

#1【初心者】知識0からEclipseでWebアプリ(Webサイト)を作る.「 Webアプリを作る為の環境を構築しよう」

はじめに

友人とWebサイトを1から作りたいという話となり,右も左もわからない状態からeclipseを使ってとりあえず触りながらのメモ書きをしていく.(調べても知ってて当然だよねって感じで細かい設定とか書いてないサイトが多い...)

環境一覧
プログラミングソフト:Eclipse 2020
JAVA SE 14
Tomcat 8.5.88

  • 環境作成編

#1 Webアプリを作る為の環境を構築しよう(この記事)2020 10月更新

#2 GitHubとEclipseを繋げて動的Webアプリを共同開発する 明日更新

  • デザインを考えよう編

  • フロントエンド作成編

  • バックエンド作成編

  • 実際にサービスを動かしてみよう編

今回の記事では,

1.Eclipseをインストールする.
2.Javaのインストール
3.パースペクティブの設定
4.プロジェクトの作成
5.サーブレットの作成
6.サーブレットの実行

という流れ.

単語メモ
パースペクティブ:各々のアプリを開発するのに最適な画面設定
サーブレット:Webサーバ上(バックエンド)で動くプログラム,今回の記事で作成するのはデバック用のローカルサーバ.

1.Eclipse をインストールする.

Eclipseには日本語化などの面倒な設定を行わずにそのまま使い始めることができる「Pleiades All in One」というエディションが用意されている.
ダウンロードサイトより,Eclips 2020をダウンロードする.

01.jpg

様々なバージョンが表示されるが,今回使用するのは Windows10 64bit の Java Full Edituonをダウンロードする.

02.jpg

勝手にダウンロードが始まる.
始まらなければ青く表示されたURLを押せばダウンロードが始まる.

03.jpg
ダウンロードした.zipファイルを解凍する.
Windowsの標準解凍ソフトだと上手く解凍できないらしいので7-Zipを使って解凍する.

04.jpg

解凍するとpleiadesというフォルダが現れるので,Windows(C:)フォルダに直接置いておく(場所は自由だが C: の直下が分かりやすい).

05.jpg

Windows(C:)直下に移動したら,C:\pleiades\eclipseフォルダ内にあるeclipse.exeを右クリックしてショートカットを作成して,デスクトップなどの分かりやすい場所に置き直しておく.(アプリとしてインストールしないためショートカットから起動する)

06.jpg

07.jpg

初回起動ではワークスペース(データの保存場所)をどこにするか聞かれるので,ドキュメントのフォルダ内にeclipseというフォルダを新規作成して,参照からフォルダを変更しておく.(デフォルトでも問題ないが分かりやすい場所に移動しておく)

08.jpg
↓ 変更する
09.jpg

変更したら起動(L)を押して画面が表示されればインストール完了.

10.jpg

2.Java SE をインストールする.

今回は JAVA SEの14を使用する.
トップページから14を選択するか,JAVA SE 14 ダウンロードサイトから Windows x64 Installer をダウンロードする(なぜかChromeだと動かない時があるのでIEを使ったほうがいい).

11.jpg

特に設定などはないのでそのままインストールするだけ.

3.パースペクティブの設定

右上のJavaアイコンの横にある①+マーク(パースペクティブ)を押すとリストが出てくるので②Java EEを選択する.

12.jpg

Java EEを開くとサーバー管理などのサポートがあるサーブレットパースペクティブ(画面構成)となる.
サーバータブなどはWebアプリを開発する際に必須なのでしっかりとJava EEに変更してから開発を開始する.

13.jpg

4.プロジェクトの新規作成

ファイル→新規→その他を選択

14.jpg

ウィザードからWebフォルダ内にある①「動的Webプロジェクト」を選び,②次へを押す.

15.jpg

設定画面が出てくるのでプロジェクト名を好きな名前で設定し,ターゲットランタイムをTomcat8 (Java8)に変更する.
変更すると動的モジュールバージョンと構成がターゲットランタイムに合わせて自動的に変更される.変更できたら次へを押す.

17.jpg

ここはデフォルトで大丈夫なので次へを押す.

18.jpg

web.xmlデプロイメント記述子の作成にチェックを入れ,完了を押すとプロジェクトが作成される.

19.jpg

これでサーブレット作成する準備が完了.

20.jpg

5.サーブレットの作成

サーブレットを作成するためにJavaリソース→srcを右クリックしてサーブレットを選択する.

21.jpg

とりあえず名前を適当に①sample_main ②sample と設定して完了を押す.

22.jpg

無事に基本的なコードが記述されたサーブレットが作成されました.

23.jpg

Webサーバのタブを開いて,「使用可能なサーバがありません.このリンクをクリックして新規サーバを作成してください.」という部分をクリックする.

24.jpg

Apacheフォルダ内から「Tomcat v8.5」サーバを選択して次へを押す.

25.jpg

サーバに追加したいリソースを①選択して,②追加を押す.

26.jpg

右側にファイルが移動しているのを確認したら完了を押す.

27.jpg

サーバーが作成されるので,右クリックして公開を押す.

28.jpg

次にもう一度右クリックして再開を押す.

29.jpg

問題がなければサーバの右側にあるステータスが[起動済み,同期済み]となる.

30.jpg

次にchromeを開いて,URLに以下を入力.

http://localhost:8080/[プロジェクト名]/[クラス名]

今回手順通りに作っていれば,

http://localhost:8080/test/sample

こんな画面が表示されれば問題なく動作している.

31.jpg

試しに

ここにある

("Served at: ")

("tesutesu")

に変えてみる(日本語で打つと文字化けするのでアルファベットで入力する).

32.jpg

しっかり更新されてますね.

33.jpg

とりあえずこれで開発環境の設定は完了,次回はGit hubで共有してみる.

次→[]

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

node.js(express)でパスワードのハッシュ化 〜bcryptモジュールの使い方〜

bcryptとは

password$2b$10$7f9myKwdo9BDUOkybKpQoOSMeEX90aRRFfOdj.c3dG6RIjZxZ/a4mみたいな感じにぐっちゃぐちゃにしてくれるnpmモジュール

使い方

まずはrequireで読み込み

const bcrypt = require('bcrypt')

ハッシュ化

const hashed = bcrypt.hash(req.body.password, 10)
//bcrypt.hash(ハッシュ化したいpassword, ストレッチング回数)
//ストレッチング回数 =ハッシュ化の回数で、2のn乗のnのことで4~31を指定できる

照合

const compared = bcrypt.compare(req.body.password, docs.password)
//bcrypt.compare(ハッシュ化前のpassword, ハッシュ化後のpassword)

ポイント

  • bcrypt.hashは時間がかかる為、非同期処理としてコントロールする必要。async awaitをつける
  • hash後にhash化したパスワードをデータベース等に保存する処理を書くことが多いと思いますが、その場合async awaitをつけないとハッシュ化が終わる前に次の保存処理を実行してしまう為、passwordがみつかりませんよ!みたいなエラーが出る。
  • bcryptにはhashSyncというメソッドも用意されており、これを使ってもasync await同様の同期的な動きをとる。が、どこかで非推奨?と見た事があります。

使用例 

現在勉強のため制作しているTodoアプリでexpress, mongooseを使っていますので、そこで使ったユーザー登録とログイン処理での使用例を記載致します。

<新規ユーザー登録>
クライアントからのフォーム送信で受けた新規ユーザーのpasswordを、ハッシュ化しデータベースに登録

router.post('/register', async (req, res) => {
  const hashedPassword = await bcrypt.hash(req.body.password, 10)

  console.log(hashedPassword);
//hashedPasswordにpromiseが返ってくるためawaitで処理の完了を待つ
//試しにawaitを外すとPromise{pending}がlogされる
//pendingは「わたし待ってます」状態

  const userData = new User({
    username: req.body.username,
    password: hashedPassword
//ここでhashedPasswordのpromiseが解決している必要がある
//そのためbcrypt.hashにawaitをつけて10回hashされるのを待ってから次の処理に進むように制御する
  })

// --- 以下DB登録処理 --- 
    User.find({ username: userData.username }, (err, docs) => {
    if (docs.length) {
      res.send({
        msg: 'そのユーザー名は使えません'
      })
      return
    } else {
      User.insertMany(userData)
      .then(() => res.send({
        msg: '登録が完了しました!ログインしてください'
      }))
    }
  })
})

<ログインユーザーの照合>
クライアントからのフォーム送信で受けたユーザーネームとpasswordをデータベースと照合し、合致するデータを取り出す

router.post('/login', (req, res) => {
// ユーザー名でデータベース検索する処理のサンプル

 User.find({
    username: req.body.username,
  }, async (err, docs) => {

    if (!docs.length) {
      res.send({
        msg: 'ユーザー名 か パスワード が間違っています',
        isLogin: false
      })
      return
    }

// ここからハッシュ前のpasswordとDB内のハッシュ済のpasswordとの照合

    const compared = await bcrypt.compare(req.body.password, docs[0].password)
// bcrypt.compare(ユーザーが入力したpassword, DBから引っ張ってきたハッシュ化済のpassword)
// bcrypt.compareもPromiseが返してくるのでasync,awaitで制御を加える
// 照合が終わったら次の処理へ進むようになる

    console.log(compared)
// passwordがあってればtrue まちがっていればfalse


    if (!hassed) {
      res.send({
        msg: 'ユーザー名 か パスワード が間違っています',
        isLogin: false
      })
      return
    } else {
      res.send({
        msg: 'ログインできました',
        isLogin: true,
        username: req.body.username
      })
    }
  })
})

以上です。

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

Reactの基礎知識

Reactの基礎知識

Reactとは?

  • Facebookが開発
  • JavaScriptのライブラリ(フレームワークではない)
  • WebのUIを作る
  • React ≠ SPA

コンポーネントとは?

UIは2つに分類される
1. 見た目(View)
2. 機能(Controller)

コンポーネント = 見た目 + 機能

Webページはコンポーネントのツリー構造になっている

なぜコンポーネントを使うのか

  • 再利用性するため
  • 分割統治するため
  • 変更に強くするため

Virtual DOM

そもそもDOMとは?

→ Document Object Modelの略
→ インターフェース
→ HTMLにアクセスする窓口
→ HTML構造、見た目、コンテンツを変更したいときはDOMを通して操作を行う

Virtual DOMとは?

Reactで管理するDOM。
通常のDOMはブラウザのレンダリングによって管理されるが
Reactではブラウザのレンダリングと別で管理を行う
→効率よくDOM操作できる

通常のDOM操作

document.getElementById('hoge').innerText='fuga';

ReactのVirtual DOM操作

render(
  <div id='hoge'>fuga</div>
);

差分描画

Reactでは変更されたVirtual DOMの差分のみを再描画する

JSX

JavaScript内でHTMLっぽく書ける

ReactDOM.render(
  <div className={hoge}>
    <h1>Hello World!</h1>
  </div>
)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jsライブラリ選定はopenbaseが超絶便利

openbaseとは

https://openbase.io
Find and compare open-source packages with user reviews, categorization, and unparalleled insights about packages' popularity, reliability, activity, and more.

手を抜いてdeeplでw

ユーザーレビュー、カテゴリ分け、パッケージの人気、信頼性、アクティビティなどについての他の追随を許さない洞察力で、オープンソースパッケージを見つけて比較してください。

現在、多くのプログラミング言語はパッケージマネージャがあって、何らかの方法でパッケージについての情報を取得することができると思います。

パッケージの選び方

自分の場合はjs系が殆どなので、ライブラリを選ぶ方法は以下のような感じです

  1. npmで検索
  2. descriptionを読んで良さそうなもの、ダウンロード数がある程度あるもの、
    最終更新が数ヶ月以内のものピックアップ
  3. GitHubページのReadmeと公式ドキュメントがどの程度整備されているか確認
    issueとPRを軽くチェックし、デモがあれば、それもチェック
    ちなみにドキュメントの有無だけでは、決めないようにしていますreact-draggableのようにドキュメントはあっさりしているけれど、定期的にリリースされていて、issue boardで開発者やコントリビュータが意見を頻繁に交換しているケースもあるため

その上で、数個に絞った上で簡単に試してみて、決めています。
激しく、外すことはないのですが、時々ざっと、Readme読んでああこれなら良さそうと思って入れて、使い始めたら勝手にあると思い込んでいた機能が実はサポートされてなくて、自分で実装するくらいならちょっとあれだけど、もう一個の最終候補に残ったやつに切り替えるかという事があります。
大抵の場合、凄い特殊な使い方というわけでは、何ので自分の他にも同じような経験の人いるだろうから、レビューとかあれば助かるなぁと思っていました。

openbaseのいいところ Review

そんな時に見つけたのが、openbase.ioです。
先にあげた選ぶ時の指標が全部入っている上に、ユーザーからのReviewも載っているという至れり尽くせりのサイトです。 
個人的には、もう少し明るい色のサイトだといいなぁとか思ったりしますが、機能面は本当に大満足です。

Reviewは投稿者のレーティング星の数と使った感想が掲載されています。
また、Review自体にLikeを付けられるので、Reviewを見た人の視点でReviewそのものをチェックしても良さそうという判断の助けになるかと。
ちなみに、Reactだと570件以上のReviewがついています

[基本情報]

[レビュー]

openbaseのいいところ tutorialをすぐ確認できる

サイドバーのtutorialをクリックすると簡単にチュートリアルにアクセスできます。Youtubeのビデオも含まれているので、ざっくりと使い方を掴むのに非常に便利です。

openbaseのいいところ 代替パッケージの情報

サイドバーのAlternativesをクリックすると、代替パッケージの情報が表示されます。下記はReactの結果なので、まぁそんなに新しい発見みたいなものはないですが、パッケージによっては面白い発見がある可能性もあるかと思います。
この機能の良いところは自分でリサーチした時の漏れをカバーすることができる点だと思います。あとは、有名なパッケージだけ、検索かけて最初から候補をAlternativesからピックアップするという方法もありではないかと。

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

TypeScriptから生成したJavaScriptを圧縮(minify)してソースマップを有効にする方法

1. はじめに

TypeScript から JavaScriptを生成する際にソースマップを生成することができます。これにより、デバッグする際にTypeScriptのソースコードに対してブレイクポイントを設定し、変数の中身を参照できます。

このJavaScriptのファイルサイズを減らすために圧縮(minify)すると、ソースマップが使えなくなるので、ソースマップを維持したままJavaScriptを圧縮する方法を説明します。

2. 環境設定

開発環境はVisual Studio Code (以後vscodeと省略)を使います。
完成形のイメージは以下の通りです。
スクリーンショット 2020-10-10 9.57.12-min.png

まず、空ディレクトリを作成し、npm からパッケージをインストールします。

npmパッケージは通常は指定ディレクトリへインストールされるが、「-g」を付けるとグローバルインストールされ、異なるディレクトリでも実行できる。

mkdir 【空ディレクトリ】
cd 【空ディレクトリ】
npm install -g typescript gulp
npm install gulp-uglify gulp-typescript gulp-sourcemaps

次に、TypeScriptの初期設定ファイルを作成します。

tsc --init

次にvscodeで上記でコマンド実行したディレクトリを開き、tsconfig.json ファイルを開きます。
そこで「"sourceMap": true」 のコメントアウトを解除してください。

  • tsconfig.json でソースマップを有効にする設定
{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",       /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "commonjs",  /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "sourceMap": true,     /* Generates corresponding '.map' file. */
  }
}

次にvscodeでtsconfig.jsonと同じ階層に「gulpfile.js」を作成し、以下のように記述してください。

const gulp = require('gulp');
const uglify = require('gulp-uglify');
const ts = require("gulp-typescript");
const sourcemaps = require('gulp-sourcemaps');

const tsProject = ts.createProject("tsconfig.json");

// gulpタスクでminify(圧縮)したjsとsourcemapを生成する。
// minifyされたjsは改行コードや空白スペースが除去され、
// 変数名が短い文字に変更されるためデバッグが困難になる。
// そこで、コンパイル元となるtypescriptとjsを結びつける
// sourcemapを用いることで、typescriptのソースを用いてデバッグできる。
// sourcemapを有効にするためには、*.ts, *.js, *.js.map の3つを
// 同じディレクトリへ配置する必要がある。
// vscode のメニューからTerminal > Run Task > gulp: default
// を選択すると、tsファイルと同じディレクトリにjsとjs.mapが生成される。
gulp.task('default', function () {
  return tsProject.src()
    .pipe(sourcemaps.init())
    .pipe(tsProject()).js
    .pipe(uglify())
    .pipe(sourcemaps.write('.', { sourceRoot: './', includeContent: false }))
    .pipe(gulp.dest("src"));
});

以上で設定完了です。
vscode からコンパイルするには、メニューのTerminal > Run Task >
「gulp: default」を選択すると、distフォルダにjsとjs.mapが生成されます。
デバッグする際には「ts, js, js.map」の3つを同じディレクトリへ入れてください。

今回説明した方法とは別に、サーバーからクライアントへ転送する際にgzip圧縮する方法があります。JavaScriptのminifyと組み合わせることで転送データ量を減らすことができます。

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

楽天のラッキーくじをちょっと使いやすくしてみた2

楽天のラッキーくじをちょっと使いやすくしてみた その2

前回の記事
楽天のラッキーくじをちょっと使いやすくしてみた

前回はトップページのPRやSPを削除してPCだけを表示するという記事を書きました。

今回は開いた先のくじをクリックするプログラムを書きました。
時々失敗するのでまだ検証中

作ってみよう

環境

macOS Catalina ver 10.15.7
GoogleChrome 86

Chromeの拡張機能

拡張機能は省略

プログラムについて

プログラムの内容

function foo(){
    let image = document.getElementById("entry");
    console.log("events");
    if(image.getAttribute("src").indexOf("entry.gif")){
        console.log("entry!")
        image.click();
    }
}
let image = document.getElementById("entry");
image.setAttribute("onload","foo()");

簡単な説明

関数foo()の中身

let image = document.getElementById("entry");
画像を取得して
console.log("events");
consoleに書き込む

if(image.getAttribute("src").indexOf("entry.gif"))
画像のsrcにentry.gifが含まれていたら
console.log("entry!")
consoleに書き込んで
image.click();
画像をクリックする

これだけじゃ画像が読み込み中とかはエラーになっちゃうので、
ページ作成時に
image.setAttribute("onload","foo()");
画像にonload時にfoo関数を呼び出すように書き込む。

これで画像が読み込まれた時に、
画像のsrcにentry.gifが含まれていたら、
画像をクリックしてくれるプログラムの完成

完成はしたんだけど...

時々失敗するので確実にするためには何か考える必要がある
sleepだと他の処理も止まる可能性があるので、
非同期関数とかよくわかってないけどお勉強してもう少し楽にできる方法を模索しましょうねという感じ...

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

Reactでvideoタグにmutedを効かせる方法(の代替)

Reactでvideoタグにmutedを添えられない問題について、
良い解決案を見かけたのでその紹介をします。

結論、mutedの指定は可能?

答えはNoです。
videoタグにmutedを添えることは、現状のreactでは実現できません。(2020/10/09現在)
(※保証されない、と表現されていますが、実際、mutedはvideoタグに反映されません。)

どうやって解決するの?

本家の該当issueにて、こんな書き込みがありました。

Mute is just equal to volume being zero
(ミュートは、ボリュームゼロと同じ。)

え、たしかに。
なぜmutedにこだわる自分が居たのかが謎になるくらい目からウロコな意見で、
実際に volume = 0 を試したらmuted相当の動きになりました :thumbsup:

ちなみに、自分は使うあてが無かったですが、onVolumeChange なども扱えるようです。

サンプルコード

↓こちら試してないですが、概念として。

const SomeVideoComponent = ({ muted }) => {
  const videoRef = useRef(null);

  ...
  useEffect(() => {
    if (videoRef.current !== null) {
      const mutedCurrently = videoRef.current.volume === 0;
      if (mutedCurrently !== muted) {
        videoRef.current.volume = muted ? 0 : 1;
      }
    }
  }, [videoRef, muted]);

  return <video ref={videoRef} />;
}

そもそもmutedを使えない背景/調査補足

理由は、こちらの本家facebook/reactのissueにて色々議論されているので、ここでは割愛します。

そのため、Qiita: React videoでmutedが消えるバグのパワー系解決 で紹介されているように、
dangerouslySetInnerHTMLなど駆使してどうにか解決しようとするライブラリもあるようです。
ただ、こういったライブラリも、スタイリングしたい場合に鬱陶しかったり、
そもそもこの些細なことのためにライブラリいる?っていう話もあると思います。

この記事もReactでMediaStream扱っている方にとって、何かの足しになればと思います。
単純な話なのですが、ちょっと呆気にとられたのでシェアでした。

それでは良き開発を〜

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