- 投稿日:2020-10-10T23:50:03+09:00
toioの磁気センサーを使って上に載せたものを検出する
サマリ
本記事を読むと、下記の動画の様にtoio™Core Cubeの磁気センサーを使って、キューブの上に載せたものを検出できるようになります。しかもp5.toioを使用しているのでブラウザと磁石があれば、どなたでもお試しは可能です!
- 例1. ピコトンズの帽子の種類を検出する
- 例2. ゲズンロイドの紙工作の種類を検出する
#ゲズンロイド の工作物も判定出来ました。これは応用のしがいがあるな。 pic.twitter.com/5jpi3JpAGY
— Tetsunori NAKAYAMA | 中山 哲法 (@tetunori_lego) October 10, 2020成果物
まだ暫定的ですが、p5.toioのv0.8.0に入れておきました。ただAPI Referenceはまだないので、すぐにアクセスして使えるサンプルとして、動画にも使った以下の2つも公開いたします。
- p5.toio Sample 11: Magnet function sample1
- p5.toio Sample 12: Magnet function sample2
※ 画面をクリックしてキューブと接続したら、磁石を後述の仕様通りの場所に近づけてください。
きっかけ
先日、toioの技術仕様書がv2.2.0へ更新され、ゆるゆるとp5.toioに新規機能を追加しています。その中で新たに磁気センサーが追加されたので、そのアプリケーションとして上に載せたものを検出することにしました。今までtoioを使った表現は主としてキューブ下のマットの読み取りを前提として検討をしていたので、上に載せるものが検出できれば、さらに現実世界で表現出来ることが増えるのではないかと思っています。
仕様のおさらい
HW仕様
まずは、公式サイトを見ましょう。
サイトによると、HW仕様としては、4mm*4mm*2mmネオジム磁石について配置をかえることで6種類のパターンで検出が可能となっています。
磁石の位置ですが、縦横方向は画像の通りでわかりやすいですが、高さ方向はキューブから少し距離(トップから3.1mm)をあける必要があるので注意です。(ハマりました)
磁石のレイアウト仕様
未装着および以下の仕様に従う 6 つのパターン、合計 7 つパターンを検出できます。
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日で到着しましたと思います。
なななんと、ネオマグさん、2020/10/11-2020/10/31で25周年記念お客様感謝キャンペーンを開催しており、全てのご注文の送料無料だそうです。
マジすか。
もう一回買っておこうかな・・・なお、Qiitaの読者様へは釈迦に説法かもしれませんが、念のため注意書きを。
今回の磁石は特に寸法が小さくかつ磁力が強いため、取り扱いによっては大変危険なものになりますので、小さなお子様やペットのいるご家庭は十分にケアをして取り扱ってください。飲み込んだりすると一大事ですよ!HW: スペーサー
前述しましたが、toioのトップ面と磁石の間には3.1mmほどのスペースが必要になります。今回の検出においてこの距離がかなりシビアだったので対応はほぼ必須と考えてください。3Dプリンタとかを持っていれば簡単かもしれませんが、うちにはなく非常に苦労しましたが、解決策を見つけました!
なんと、みんな大好きレゴブロックのパネルパーツをスペーサーとして取り付けるだけで、すべて解決しました!
キューブの天面にパネルをつけると、天面からパネルまでの距離が3.2mm位となりますので、ほぼ仕様通りで塩梅が良いです。
なお、レゴのパネルパーツのサイズですが、基本はキューブ天面の、磁石検出付近のリボン型が割れている箇所にパネルが付いていればよいので、1*2, 1*4, 2*4どれでもOKです。
ただ、今後キューブに上物をかぶせた時、上物が傾く可能性があるので、下の写真の様に2*4パーツを縦方向に取り付けるか、全面をパネルで覆う方がベターです。
このパネルさえあれば、上に磁石を載せるだけで検出できるようになります。
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キューブスキン検出
まずは仕様通りの磁石配置で機能を確認したいと思います。磁石の検出位置が本当にシビアなので、毎回合わせるのはつらく、パターンごとにキューブスキンを作って磁石を貼り付けることにしました。
こんな感じ。
これを実際に検出にかけてみると、、良い感じです。
後でQiitaから参照する用。#toio #p5toio pic.twitter.com/h0K0UIBInu
— Tetsunori NAKAYAMA | 中山 哲法 (@tetunori_lego) October 10, 2020
なかなか反応せず、もどかしい場合や、キューブの個体差があるように感じますが、上々の出来栄えです。応用1:ゲズンロイド工作検出
上記を応用、というか上に工作物載せただけですが、こういう原理でゲズンロイドの工作検出を実現しました。
キューブの上のレゴパネルは必須です。
応用2:ピコトンズ帽子検出
こっちは割とテクニカルですが、帽子の中に湾曲させた紙を貼りそこに磁石を取り付けています。
この位置をピンポイントで狙うとパネルパーツなしで検出がうまくいきます。
番外編:おうちにある磁石で検出してみる
家の冷蔵庫にネオジム磁石はたくさん貼ってあるので、それで動作しないのか確認してみました。
結論として、磁力に応じてスペースを変えることを意識すれば、結構問題なく検出できることが分かりました。
これらが、我が家にあったネオジム磁石たち。すべて検出可能でした。
意外とイケるんだなと。
なお、フェライト磁石は反応しませんでした。所感と考察
- 最初にピコトンズ帽子の件を種明かしせずにtwitterにあげましたが、どうなっているの?とびっくりいただけた方もいらっしゃったので、それなりにインパクトはあって良かった。
- 磁石位置については、かなりシビアで手作業でやるにはちょっと難しく感じました。どなたか3Dプリンタで磁石の凹み付きのデータ公開してくれるとありがたいなと思いました。
- 磁気センサーはアイディア次第で他にもいろいろできそう。p5.toioなら、ブラウザからアクセスするだけで使えるのでぜひ皆様も。
- 投稿日:2020-10-10T23:12:17+09:00
Reactコンポーネントの雛形生成を自動化するスクリプトファイルを作る
エンジニアにとって無駄な作業はストレス。
繰り返しの作業を自動化したい。という事でコンポーネントファイルを自動生成するスクリプトを書いた。
スクリプトファイルの使い方
まずは使い方から。
ルートディレクトリでbash ./etc/scripts/make-component-template.sh components Layoutというように、ディレクトリ名とコンポーネント名を指定するだけ。
ルートディレクトリ直下にcomponents
ディレクトリがある事を想定しています。
自動生成するファイル
./etc/scripts/make-component-template.sh components Layout
を実行すると以下のファイルを生成します。components └── Layout ├── Layout.jsx ├── index.jsx └── style.cssファイルの中身
index.jsxexport { default } from './Layout'Layout.jsximport 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 Containerstyle.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.txtimport 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!?
- 投稿日:2020-10-10T22:25:31+09:00
【JavaScript】すぐ命令してくる夫に困っています...
『しかも、何度も同じことばかり命令するんです.....』
夫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
は命令です。 例えるならトロッコに乗るように、線路に沿って進んだ先に分岐があります。
「右へ行け」「左へ行け」とマシンに命令しているのです。では何が辛いかというと、トロッコにずっと乗ってないといけないのが辛いです。
条件を確認して、あっちへ行ったりこっちへ行ったり、時には線路を戻って(アレ? なんの条件にしたがってたんだっけ???)となったり。
常に頭の中でフローを意識しなければならないのは、かなり体力を使います。
宣言で書き直す
必要なデータをあらかじめ用意しておくことで、[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つの分岐を扱っています。
- テスト環境(
test1
/test2
)の分岐- プラットフォーム(
ios
/android
)の分岐こういった場合、関心をひとつづつ分ける方が読みやすく保守しやすいコードになることがあります。
const host = env === 'test1' ? 'aaa' : 'bbb'; const platform = os === 'ios' ? 'apple' : 'google'; const url = `https://${host}.example.com/${platform}`; setUrl(url);最初のコードと比べて、ずいぶん短くすっきりした印象です!
まとめ
命令ではなく、宣言で表現することで、簡潔にできることが多々あるようです。
- 必要な処理やデータをあらかじめ用意する
- 複数の関心は、分離する
という意識が必要な気がしています。
いろいろなバリエーションを使いこなして、見通しの良い記述をしたいですね。
- 投稿日:2020-10-10T21:46:47+09:00
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が出力される。 */参考
- 初めてのJavaScript ES2015以降の最新ウェブ開発
- JavaScript Promiseの本
- 投稿日:2020-10-10T21:34:16+09:00
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
- 投稿日:2020-10-10T20:03:53+09:00
[初心者向け]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つのセットにして、他の機能とは分離して開発できるようにする仕組み
- 投稿日:2020-10-10T19:47:58+09:00
ReactHookFormの導入と簡単な使い方
はじめに
今回はReactHookFormの導入と簡単な解説をしていこうと思います。
Reactの環境構築はできているものとします。一応、JavasciptとTypescriptの両方のコードを載せているので自分が使っている方を見てください。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.jsximport 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.jsximport 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.tsximport 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で入力の有無の状態を取得する方法をまとめたいと思います。
- 投稿日:2020-10-10T19:03:34+09:00
Vue + Wordpress + Heroku + S3でポートフォリオを構築する
1. はじめに
こんにちは。ツダと申します。私はカメラが趣味で、自分の写真のポートフォリオサイトを作成したいと思い、Vue.jsとWordpressを使って作成しました。
この記事では、私がポートフォリオを作るうえで行ったことについて紹介させていただければと思っています。
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
簡単なシーケンス図(トップ画面表示時)
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.phpfunction 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へのアクセス、およびログイン画面へのアクセスは必要なので許可し、それ以外を拒否するように設定しています。実際にアクセスしてみると下のような拒否画面が現れます。
参考:WordPress へのアクセス制限を functions.php で設定する
3.5 S3を使うための設定をする
Herokuは定期的にビルドを行っているため、アップロードした画像が消えてしまう現象が発生します。
herokuのリポジトリがdynoという単位で管理されており、Freeプランだと一定時間経過後にdynoが再起動される仕様になっているから。 - 【Rails6】herokuで投稿した画像が表示されない
そこでAWSの「S3」というサービスを用いることで画像が消える現象を回避していきます。Wordpressのプラグインを用いてアップロードする画像の格納先をS3に設定することで、外部に画像を保存します。
S3を使用するための準備をする
S3のアクセス権限の設定
ブロックパブリックアクセスは下記のように設定。
この設定によって以下の二つを達成することを確認しております。
- WordPressからの画像アップロード
- ポートフォリオサイトからの画像の表示
バケットポリシーの設定は次のようにしています。
バケットポリシー{ "Version": "2008-10-17", "Statement": [ { "Sid": "AllowPublicRead", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": "s3:GetObject", "Resource": "<リソース名>/*" } ] }参考:
- WordPress S3 Tutorial: How to Connect WordPress to Amazon S3 Bucket
- Amazon S3 コンソールを使用して、バケットの ACL でパブリックアクセスを有効にしました。誰でもバケットにアクセスが可能ですか ?
ローカル環境で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 expiredGDをComposerに追加する
WordpressでS3を使用するためにはGD(画像ファイルの操作ライブラリ)の使用が必要です。HerokuにはデフォルトでGDがインストールされてないため、Composerで使えるように設定する必要があります。
まず、composer.jsonに下記のように追記します。
composer.json{ "require": { "ext-gd": "*" } }作ったファイルはwordpressのルートディレクトリに配置します。続いて下記のコマンドを実行します。
$ composer install終了後、composer.lockがcomposer.jsonと同じディレクトリに作成されていればOKです。後はHerokuへのデプロイ時にHerokuが自動的にライブラリをインストールします。
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の管理画面左にあるタブから「設定→パーマリンク設定」と進み、共通設定の「投稿名」にチェックをいれるだけとなっております。
これで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 Router4.3 アプリの全体像
アプリの全体像は次のようになっています。
main.jsimport 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.jsimport { 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)次にメニュー部分の実装に入ります。
参考:NavbarTop.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.vuemounted() { 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.vuedata() { return { posts: [], // フェードイン実装のために必要 ok: false } },そしてマウント時にtrueとすることで、要素を表示させます。
Articles.vuemounted() { ...(省略)... (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です。
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.jsvar 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にスケジューラを追加
まずはアプリケーションフォルダ配下で下記コマンドを実行し、スケジューラのアドオンを追加していきます。
$ 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すると下記のような画面が表示されるはずです。
「Create Job」を押下します。
すると下記のようなウィンドウが表示されますので、
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.inipost_max_size = 100M upload_max_filesize = 100MそしてこのファイルをWordpressのルートディレクトリに配置します。後はアプリをデプロイするとHerokuが自動で設定値を認識し、画像のアップロードサイズが増加しているはずです。
6. まとめ
以上がVue + Wordpressを用いてポートフォリオを作成した手順となります。初めてVueを用いてWebサイトを作成しましたが、APIの併用もしたことで大変勉強になりました。簡単にはなってしまいますが、以上で記事を締めさせていただこうと思います。最後に、参考にさせていただいた記事の執筆者の方々、本当にありがとうございます。そして勝手に引用させていただき恐縮です。
備考
今回紹介させていただいたポートフォリオは無料で作成することが可能となっております。しかしSSLを常用化したり、独自ドメインを設定したい場合は有料となってしまいます。
参考させていただいたリンクまとめ
Wordpress
WP REST API
WP REST APILocalByFlyweelでの環境構築
超簡単にローカル環境が構築できるLocalbyFlywheelの使い方
Local by Flywheelのダウンロードからインストールまでの手順Wordpress + HerokuのDB設定
PaaS入門 〜Heroku + wordpress〜Login Rebuilder(ログインパスの変更)
Login Rebuilderの使い方|WordPressのログインURLの変更方法phpのアクセス制限
WordPress へのアクセス制限を functions.php で設定するWP REST APIのためのパーマリンク設定
WP REST APIで404が返ってくる。これはパーマリンク設定のせいだ!S3
WordPressの画像データをS3に保存する
Amazon S3 パブリックアクセスブロックの使用
WordPress S3 Tutorial: How to Connect WordPress to Amazon S3 Bucket
Amazon S3 コンソールを使用して、バケットの ACL でパブリックアクセスを有効にしました。誰でもバケットにアクセスが可能ですか ?
CA certificates extracted from Mozilla
curl: (60) SSL certificate problem: unable to get local issuer certificate
cURL error 60: SSL certificate problem: certificate has expiredGD(PHPの画像操作ライブラリ)
Image Processing and GDcomposer
Heroku PHP SupportHerokuへのデプロイ
Heroku初心者がHello, Herokuをしてみる
PaaS入門 〜Heroku + wordpress〜Vue.js
Vue.js環境構築
Vue.js を vue-cli を使ってシンプルにはじめてみる
axios - npm
Getting Started | BootstrapVue
Installation | Vue RouterBootstrapVue
Getting Started | BootstrapVueVueでNavbar
Navbarフェードインの実装
Enter/Leave & List TransitionsWordpressを用いた記事取得
《WordPress》2017年末にWP REST API で取得してVue.jsで描画するまでのまとめ。Herokuへのデプロイ
Vue.jsで作ったアプリをHerokuにデプロイrouterの設定
HTML5 History モード
connect-history-api-fallback
vue-routerのルーティングURLからハッシュを除去しつつ、URL直接指定でも表示させる(Node, Express)その他
Herokuのスリープ防止
herokuを24時間稼働させる設定Herokuでphp.iniを書き換える
Customizing Web Server and Runtime Settings for PHP
- 投稿日:2020-10-10T18:32:01+09:00
動かして理解するasync/await
JavaScriptのasync/awaitを動かしながら理解したい人向けです。
Node.jsで実行できます。async/awaittest() // 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
- 投稿日:2020-10-10T17:36:17+09:00
?【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
しましょう
- 投稿日:2020-10-10T17:02:11+09:00
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で追加するやり方の方が良いのかもしくは別のやり方があるのかを知りたいです。
よろしくお願いします。
- 投稿日:2020-10-10T16:34:16+09:00
【Javascript】ハンバーガーメニュー作成(左、上、右から表示)
目標
開発環境
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>
内部に記述してください
実際のコード(上から)
#nav{ position: absolute; height: 300px; width: 40%; left: 0; /* 変更 */ top: -300px; /* 変更 */ background: #ffffff; transition: .7s; } .in{ transform: translateY(100%); /* 変更 */ }実際のコード(右から)
#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参考
- 投稿日:2020-10-10T16:23:30+09:00
【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`の要素を削除 });さいごに
配列の要素追加・削除は
arrayUnion
arrayRemove
が便利です!配列取得(get)→配列加工→更新(update)のように、排他制御的のためのトランザクションが必要なくなるので、積極的に使ってみてください。
参考
https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja
- 投稿日:2020-10-10T15:14:16+09:00
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
- 投稿日:2020-10-10T14:53:54+09:00
【IE限定】JavaScriptからCOMを呼び出す
IEからCOMを呼び出す方法です。
最近やったので、ノウハウをまとめます。
IE離れが進む中で今更こんなことする機会は滅多にないと思いますが、やるとなるとレガシー過ぎて情報が少なく、苦労すると思います。開発環境
開発環境は以下の通りです。
- Windows10
- Visual Studio 2019
- IE11
- C++
インストール
Visual Studio 2019をインストールします。
今回は個人の勉強用として、Visual Studio Community 2019をインストールしました。
基本、インストーラに従って、デフォルトのまま、インストールすればよいですが、「ワークロード」で「C++ によるデスクトップ開発」を選んでください。
プロジェクト作成
Visual Studio Community 2019のインストールが終わったら、プロジェクトを作成します。
Windowsの検索窓で「visual」と入力すると、Visual Studio 2019が候補に表示されます。
右クリックでメニュー表示し、「管理者として実行」を選択してください。
Visual Studioが起動したら、「新しいプロジェクトの作成」からプロジェクトを作成します。
「新しいプロジェクトの作成」画面が表示されるので、検索欄に「atl」と入力して、「ATL プロジェクト」を検索します。
「ATL プロジェクト」が表示されたら、選択して、次に進みます。
あとは、デフォルトのままプロジェクト作成を進めてよいですが、「アプリケーションの種類」が「ダイナミック リンク ライブラリ (.dll)」であることは、一応、確認してください。
実装
プロジェクトが作成できたら、実装していきます。
COM側の実装
シンプルオブジェクトの作成
まずは、シンプルオブジェクトを追加します。
シンプルオブジェクトを追加すると、実装に必要なファイルが生成されます。
Visual Studioの「ソリューション エクスプローラー」から、プロジェクトを選択します。
右クリックでメニューを表示し、「追加」→「新しい項目」を選択します。
「新しい項目の追加」画面が表示されます。
「インストール済み」から「ATL」を選択し、「ATL シンプル オブジェクト」を選択します。
「追加」ボタンをクリックして次に進みます。
「ATL シンプル オブジェクト」画面が表示されます。
自動生成されるファイルの名前が表示されます。
今回は名前を変更せずに進めたいと思います。「完了」ボタンをクリックします。
これで実装に必要なファイルが作成されます。
ファイルに実装を記述
作成されたファイルに実装を記述していきます。
記述が必要なファイルは、以下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";プロパティについて
メンバ変数に外部から値を取得・設定するためのアクセサです。
propget
・propput
が、値取得・設定のセットで、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を管理者モードで起動していること
「ソリューション エクスプローラー」でプロジェクトを選択し、右クリックでメニューを開き、「リビルド」を選択します。
ビルドが終了すると、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で開くと、以下のメッセージが表示されます。
「ブロックされているコンテンツを許可」をクリックします。
次に以下のメッセージが表示されます。
「はい」をクリックします。
JavaScriptからCOMにアクセスが行われ、結果がアラートダイアログで表示されます。
- 投稿日:2020-10-10T14:42:04+09:00
再帰関数を使って数値文字列(負数、小数含む)をカンマ区切りにする処理
練習でアプリを作成しているときに作ったコード。
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.jsconsole.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 > 1
をarray[1]
に変えれば末尾の小数点も省けます)
toLocaleString()
や正規表現を使わなかった理由
0.
とか0.0
など末尾が小数点だったり小数点以下に0
しかない文字も表示できるようにしたいけど、toLocaleString()
だとこのような文字は小数点以下が省略されてしまう。- 正規表現使えば数行で実装できるらしい、けど正規表現良く分からないし理解できてないものをコピペして使うのは避けたい。
- for文でもいいけど再帰関数の方が三項演算子使ってスマートに書ける。
あとlet
使わないで済む。参考
- 投稿日:2020-10-10T13:31:00+09:00
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>
- 投稿日:2020-10-10T11:51:33+09:00
初心者がReact & Firebaseを使って収支管理アプリを作成(解説編)
今回作ったもの
ログイン機能付きの収支管理アプリです。
いわゆる家計簿アプリ的な物です。
毎月の収入と支出をリスト化し月の残高を表示します。感想編については、別途記事を書いてるので、ご興味ございましたら是非ご覧下さいませ!
初心者が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 startFirebaseの準備
プロジェクトを作成
作成方法は手順に従えば大丈夫です。
簡単なので割愛しますが、全体の流れはこちらのYoutubeを参考にしました。
日本一わかりやすいReact入門【実践編】#3...Firebaseプロジェクトの作成と初めてのデプロイ注意点として、node.jsとfirebaseのバージョンにより少し挙動が動画内容と異なります。
ポイントは、
- ロケーション設定は初めにやる。東京なら「asia-northeast1」
- プランは現在の安定node versionだとSparkではなくBlazeプラン(従量制)になります
- Blazeプランなので、念の為Google Cloud Platformを作成して、アクセス& 料金状況/アラーム通知を受信できるよう設定します(任意)
Authenticationの設定
Sign-in methodよりステータスを有効にする
今回は「メール/パスワード」を使用
Cloud Firestoreでデータベース作成
- テストモードで実行
- とりあえずコレクション/ドキュメントを追加してみる(イメージのため)
最終的なコレクション/ドキュメント構成はこちらです。
Firebaseの設定
Firebaseの情報をwebアプリに登録します。
プロジェクト内容は一応セキュリティを考慮し.env
に登録します。
.env
をgitignore
すれば公開される心配がないということですね。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の初期化処理が行われます。
auth
とdb
も作り、毎回全部書かなくて済むようにします。src/firebase/Firebase.jsimport 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.jsconst 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.jsimport 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
関数
引数にpassword
、history
を渡して非同期処理を行います。
auth.createUserWithEmailAndPassword(email, password)
は、
firebaseのメソッドでpassword
を元にアカウントが作成されます。
その後user
の情報を取得し、CurrentUser
にセットします。
history.push("/")
は、reactRouterの画面遷移させる機能です。
今回はログイン後、Home画面に行きます。
login
関数
同じ様に、ユーザーがログインしたら情報を取得しCurrentUser
を更新するようにします。
auth.signInWithEmailAndPassword(email,password)
これもまたfirebaseのメソッドです。あとは、最初にログインしてるか確認する為、
useEffect
で一回だけauth.onAuthStateChanged
を実行します。
※一回だけ実行したいので、第二引数には空の配列[]
を渡します。PrivateRoute.js
アプリのメイン画面
Home.js
は、PrivateRoute
に指定します。
ここで、ユーザーがログインしてれば→メイン画面を表示。
未ログインの場合→ログイン画面を表示。の作業を行なってます。src/auth/PraveteRoute.jsimport 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
が実行される時、入力されたpassword
の内容をAuthProvider
で作ったsignup
関数の引数に渡してデータが登録されます。
アップデート後のhistory(情報)を渡すために、最後exportの時withRouter(SignUp)
を使っています。src/auth/SignUp.jsconst 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.jsconst 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ステートの作成/更新
親コンポーネントから子コンポーネントに渡す為、ステートは全て
Home.js
で作成します。作成したステート達
src/compoments/Home.jsconst [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.jsconst 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
を用意。引数にはユーザーが入力したtext
とamount
を渡す。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.jsconst 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
を作成- その配列にドキュメントの
data
とid
をpush
(追加)する- reactアプリ側の
incomeItems
の配列を更新するポイントは、
orderBy
のメソッドでdate
を昇順にしてます。
リストがバラバラに表示されてしまうので、制御する為に必要です。
また、orderBy
で昇順にしようとするとfirebaseから「indexを作れ」というエラーが出ます。
その際、親切にURLが表示されるのでそこにアクセスしてindexを作ればOKです(結構時間かかります)
参考記事:複合index
startAt
とendAt
は、その月の分だけ表示させる為です。
この引数の中身については、後ほど詳細を書きます。これと同じ内容で出費expense用の関数も用意すれば、無事Firestoreからデータを取得できて、Reactのステートに更新/表示がされます。
尚、データを取得するタイミングは、
useEffect
を使って操作します。
- 最初の1回のみ実行してほしい。引数は空の配列。
date
が更新されるたびに実行してほしい。※date
は次で解説してますがヘッダーの月の部分です。src/compoments/Home.jsuseEffect (() => { getIncomeData(); getExpenseData(); }, []); useEffect(() => { getIncomeData(); getExpenseData(); }, [date]);月ごとにデータを表示させる
月ごとにデータを分けて表示させる為、ステートで作った
date
を使います。
初期値は現在の日時が入ってます。src/compoments/Home.jsconst [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
では、現在の月を表示させる為、year
とmonth
を作り、
隣に前月と次月ボタンを表示させます。src/compoments/Header.jsconst 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.jsexport 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
の引数に ユーザーの入力内容inputText
とinputAmount
を渡すexp
タイプも同様→Firestoreのデータが追加され、reactアプリのステートも更新されるという流れ。
ヘッダーが今月なのか、今月でない月かによって表示方法を変える為、条件付きレンダーを行ってます。
(※今月のみ追加フォームを表示させる為)この条件に使ってる
selectedMonth
とthisMonth
は、
他のコンポーネント(リスト)でも使うので、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
を行い、IncomeItem
とExpenseItem
をそれぞれ表示します。src/components/ItemsList.jsexport 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.jsexport 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.jsconst deleteIncome = (docId) => { db.collection('incomeItems').doc(docId).delete() }firebaseのメソットを使い、
incomeItems
コレクションにある、該当ドキュメントを削除してます。あとは、スタイル上、表示の仕方を変えたいので、ここでも条件付きレンダーを行ってます。
→showThisMonth
とshowPastMonth
で分ける。
これで今月以外、削除ボタンを表示させないようにしてます。これと同じようにexpenseバージョンも作ればOK
収入/支出の値を計算する
収入と支出の各合計
値の計算はそれぞれの
incomeItems
とexpenseItems
の配列から、amount
を取り出して計算します。src/components/IncomeExpense.jsexport 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> ) }
map
とreduce
については、こちらを元に別で記事を書いています。
※filter
は途中色々変えたので、結局今回のアプリに使っていません。支出expenseの合計計算
-expenseAmount
にexpenseItems
の中のamount
だけ取り出した配列を代入します。
-expenseAmount
を使って、累計を計算し、expenseTotal
とします。これを収入
incomeItems
でも同じことをします。
ただincomeバージョンについては、他のコンポーネントでも使うので、共通関数ファイルTotalIncome.js
を作成してます。
TotalIncome.js
で関数totalCalc
を作り、Home.js
でincomeItems
を引数に渡してます。src/components/Home.js// calculate % and show total const incomeTotal = totalCalc(incomeItems);src/components/TotalIncome.jsexport const totalCalc = (incomeItems) => { const incomeAmounts = incomeItems.map(incomeItem => incomeItem.amount); return incomeAmounts.reduce((acc, cur) => acc += cur, 0); };こちらを使う他のコンポーネントとは、
割合%を表示するIncomeExpense.js
、ExpenseItem
と、計算に必要なBalance.js
になります。残高の計算
総合計の残高を計算をする
Balance.js
コンポーネントでは、
IncomeTotal - ExpenseTotal
をすれば計算できます。src/components/Balance.jsconst balance = incomeTotal - expenseTotalアプリを公開
あとは公開するだけです!
私の場合はfirebase login
とinit
は先に済ませてました。> firebase login > firebase init - 選択項目は FireStore, Functions, Hosting - publicディレクトリはbuildにする > npm run build > firebase deployあとがき
今回のアプリは全てデータをpropsで親から子供に渡しているので、
どうしてもメインのHome.js
が少しボリューミーになってしまいました。(そういうものなのか?)
初心者が書いたコードですので、ご理解いただければと思います。
こうした方が良い等ありましたら、是非ご指摘お願いします!
日々勉強して、もっと良い書き方でコードを書けるよう頑張ります。参考
- 投稿日:2020-10-10T11:49:59+09:00
【JS学習その⑩】JavaScriptにおけるgetter/setterとstatic
JS学習シリーズの目的
このシリーズは、私ジャックが学んだJavaScriptのメカニズムについてアウトプットも兼ねて、
皆さんと知識や理解を共有するためのものです。
(理解に間違いがあればご指摘いただけると幸いです)getter/setterとは
- getter => 「プロパティが参照された際に行う処理」
- setter => 「プロパティに変更があった際に行う処理」
※プロパティにはディスクリプターというものがあり、その中にgetとsetがある
main.jsfunction 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.jsclass 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.jsclass 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.jsfunction Person1(name, age) { this._name = name; this._age = age; } Person1.hello = function() { console.log('hello'); } Person1.hello(); /*hello*/よってクラス内で
static
を使って定義した静的メソッドは上記のようにして書いたものと同じということです。まとめ
いかがでしたでしょうか。
getter,setterとstatic(静的メソッド)はJavaScriptにおいて重要なのでしっかり理解しておきましょう!
- 投稿日:2020-10-10T09:38:29+09:00
#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をダウンロードする.様々なバージョンが表示されるが,今回使用するのは Windows10 64bit の Java Full Edituonをダウンロードする.
勝手にダウンロードが始まる.
始まらなければ青く表示されたURLを押せばダウンロードが始まる.
ダウンロードした.zipファイルを解凍する.
Windowsの標準解凍ソフトだと上手く解凍できないらしいので7-Zipを使って解凍する.解凍するとpleiadesというフォルダが現れるので,Windows(C:)フォルダに直接置いておく(場所は自由だが C: の直下が分かりやすい).
Windows(C:)直下に移動したら,C:\pleiades\eclipseフォルダ内にあるeclipse.exeを右クリックしてショートカットを作成して,デスクトップなどの分かりやすい場所に置き直しておく.(アプリとしてインストールしないためショートカットから起動する)
初回起動ではワークスペース(データの保存場所)をどこにするか聞かれるので,ドキュメントのフォルダ内にeclipseというフォルダを新規作成して,参照からフォルダを変更しておく.(デフォルトでも問題ないが分かりやすい場所に移動しておく)
変更したら起動(L)を押して画面が表示されればインストール完了.
2.Java SE をインストールする.
今回は JAVA SEの14を使用する.
トップページから14を選択するか,JAVA SE 14 ダウンロードサイトから Windows x64 Installer をダウンロードする(なぜかChromeだと動かない時があるのでIEを使ったほうがいい).特に設定などはないのでそのままインストールするだけ.
3.パースペクティブの設定
右上のJavaアイコンの横にある①+マーク(パースペクティブ)を押すとリストが出てくるので②Java EEを選択する.
Java EEを開くとサーバー管理などのサポートがあるサーブレットパースペクティブ(画面構成)となる.
サーバータブなどはWebアプリを開発する際に必須なのでしっかりとJava EEに変更してから開発を開始する.4.プロジェクトの新規作成
ファイル→新規→その他を選択
ウィザードからWebフォルダ内にある①「動的Webプロジェクト」を選び,②次へを押す.
設定画面が出てくるのでプロジェクト名を好きな名前で設定し,ターゲットランタイムをTomcat8 (Java8)に変更する.
変更すると動的モジュールバージョンと構成がターゲットランタイムに合わせて自動的に変更される.変更できたら次へを押す.ここはデフォルトで大丈夫なので次へを押す.
web.xmlデプロイメント記述子の作成にチェックを入れ,完了を押すとプロジェクトが作成される.
これでサーブレット作成する準備が完了.
5.サーブレットの作成
サーブレットを作成するためにJavaリソース→srcを右クリックしてサーブレットを選択する.
とりあえず名前を適当に①sample_main ②sample と設定して完了を押す.
無事に基本的なコードが記述されたサーブレットが作成されました.
Webサーバのタブを開いて,「使用可能なサーバがありません.このリンクをクリックして新規サーバを作成してください.」という部分をクリックする.
Apacheフォルダ内から「Tomcat v8.5」サーバを選択して次へを押す.
サーバに追加したいリソースを①選択して,②追加を押す.
右側にファイルが移動しているのを確認したら完了を押す.
サーバーが作成されるので,右クリックして公開を押す.
次にもう一度右クリックして再開を押す.
問題がなければサーバの右側にあるステータスが[起動済み,同期済み]となる.
次にchromeを開いて,URLに以下を入力.
http://localhost:8080/[プロジェクト名]/[クラス名]今回手順通りに作っていれば,
http://localhost:8080/test/sampleこんな画面が表示されれば問題なく動作している.
試しに
ここにある
("Served at: ")を
("tesutesu")に変えてみる(日本語で打つと文字化けするのでアルファベットで入力する).
しっかり更新されてますね.
とりあえずこれで開発環境の設定は完了,とりあえず次回はGit hubで共有してみる.
次→[]
- 投稿日:2020-10-10T09:38:29+09:00
#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をダウンロードする.様々なバージョンが表示されるが,今回使用するのは Windows10 64bit の Java Full Edituonをダウンロードする.
勝手にダウンロードが始まる.
始まらなければ青く表示されたURLを押せばダウンロードが始まる.
ダウンロードした.zipファイルを解凍する.
Windowsの標準解凍ソフトだと上手く解凍できないらしいので7-Zipを使って解凍する.解凍するとpleiadesというフォルダが現れるので,Windows(C:)フォルダに直接置いておく(場所は自由だが C: の直下が分かりやすい).
Windows(C:)直下に移動したら,C:\pleiades\eclipseフォルダ内にあるeclipse.exeを右クリックしてショートカットを作成して,デスクトップなどの分かりやすい場所に置き直しておく.(アプリとしてインストールしないためショートカットから起動する)
初回起動ではワークスペース(データの保存場所)をどこにするか聞かれるので,ドキュメントのフォルダ内にeclipseというフォルダを新規作成して,参照からフォルダを変更しておく.(デフォルトでも問題ないが分かりやすい場所に移動しておく)
変更したら起動(L)を押して画面が表示されればインストール完了.
2.Java SE をインストールする.
今回は JAVA SEの14を使用する.
トップページから14を選択するか,JAVA SE 14 ダウンロードサイトから Windows x64 Installer をダウンロードする(なぜかChromeだと動かない時があるのでIEを使ったほうがいい).特に設定などはないのでそのままインストールするだけ.
3.パースペクティブの設定
右上のJavaアイコンの横にある①+マーク(パースペクティブ)を押すとリストが出てくるので②Java EEを選択する.
Java EEを開くとサーバー管理などのサポートがあるサーブレットパースペクティブ(画面構成)となる.
サーバータブなどはWebアプリを開発する際に必須なのでしっかりとJava EEに変更してから開発を開始する.4.プロジェクトの新規作成
ファイル→新規→その他を選択
ウィザードからWebフォルダ内にある①「動的Webプロジェクト」を選び,②次へを押す.
設定画面が出てくるのでプロジェクト名を好きな名前で設定し,ターゲットランタイムをTomcat8 (Java8)に変更する.
変更すると動的モジュールバージョンと構成がターゲットランタイムに合わせて自動的に変更される.変更できたら次へを押す.ここはデフォルトで大丈夫なので次へを押す.
web.xmlデプロイメント記述子の作成にチェックを入れ,完了を押すとプロジェクトが作成される.
これでサーブレット作成する準備が完了.
5.サーブレットの作成
サーブレットを作成するためにJavaリソース→srcを右クリックしてサーブレットを選択する.
とりあえず名前を適当に①sample_main ②sample と設定して完了を押す.
無事に基本的なコードが記述されたサーブレットが作成されました.
Webサーバのタブを開いて,「使用可能なサーバがありません.このリンクをクリックして新規サーバを作成してください.」という部分をクリックする.
Apacheフォルダ内から「Tomcat v8.5」サーバを選択して次へを押す.
サーバに追加したいリソースを①選択して,②追加を押す.
右側にファイルが移動しているのを確認したら完了を押す.
サーバーが作成されるので,右クリックして公開を押す.
次にもう一度右クリックして再開を押す.
問題がなければサーバの右側にあるステータスが[起動済み,同期済み]となる.
次にchromeを開いて,URLに以下を入力.
http://localhost:8080/[プロジェクト名]/[クラス名]今回手順通りに作っていれば,
http://localhost:8080/test/sampleこんな画面が表示されれば問題なく動作している.
試しに
ここにある
("Served at: ")を
("tesutesu")に変えてみる(日本語で打つと文字化けするのでアルファベットで入力する).
しっかり更新されてますね.
とりあえずこれで開発環境の設定は完了,次回はGit hubで共有してみる.
次→[]
- 投稿日:2020-10-10T07:48:11+09:00
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 }) } }) })以上です。
- 投稿日:2020-10-10T06:35:52+09:00
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> )
- 投稿日:2020-10-10T05:32:48+09:00
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系が殆どなので、ライブラリを選ぶ方法は以下のような感じです
- npmで検索
- descriptionを読んで良さそうなもの、ダウンロード数がある程度あるもの、
最終更新が数ヶ月以内のものピックアップ
- 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からピックアップするという方法もありではないかと。
- 投稿日:2020-10-10T02:59:41+09:00
TypeScriptから生成したJavaScriptを圧縮(minify)してソースマップを有効にする方法
1. はじめに
TypeScript から JavaScriptを生成する際にソースマップを生成することができます。これにより、デバッグする際にTypeScriptのソースコードに対してブレイクポイントを設定し、変数の中身を参照できます。
このJavaScriptのファイルサイズを減らすために圧縮(minify)すると、ソースマップが使えなくなるので、ソースマップを維持したままJavaScriptを圧縮する方法を説明します。
2. 環境設定
開発環境はVisual Studio Code (以後vscodeと省略)を使います。
完成形のイメージは以下の通りです。
まず、空ディレクトリを作成し、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と組み合わせることで転送データ量を減らすことができます。
- 投稿日:2020-10-10T01:54:26+09:00
楽天のラッキーくじをちょっと使いやすくしてみた2
楽天のラッキーくじをちょっと使いやすくしてみた その2
前回の記事
楽天のラッキーくじをちょっと使いやすくしてみた前回はトップページのPRやSPを削除してPCだけを表示するという記事を書きました。
今回は開いた先のくじをクリックするプログラムを書きました。
時々失敗するのでまだ検証中作ってみよう
環境
macOS Catalina ver 10.15.7
GoogleChrome 86Chromeの拡張機能
拡張機能は省略
プログラムについて
プログラムの内容
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だと他の処理も止まる可能性があるので、
非同期関数とかよくわかってないけどお勉強してもう少し楽にできる方法を模索しましょうねという感じ...
- 投稿日:2020-10-10T01:26:07+09:00
Reactでvideoタグにmutedを効かせる方法(の代替)
Reactでvideoタグにmutedを添えられない問題について、
良い解決案を見かけたのでその紹介をします。結論、mutedの指定は可能?
答えはNoです。
videoタグにmutedを添えることは、現状のreactでは実現できません。(2020/10/09現在)
(※保証されない、と表現されていますが、実際、mutedはvideoタグに反映されません。)どうやって解決するの?
Mute is just equal to volume being zero
(ミュートは、ボリュームゼロと同じ。)え、たしかに。
なぜmutedにこだわる自分が居たのかが謎になるくらい目からウロコな意見で、
実際にvolume = 0
を試したらmuted相当の動きになりましたちなみに、自分は使うあてが無かったですが、
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扱っている方にとって、何かの足しになればと思います。
単純な話なのですが、ちょっと呆気にとられたのでシェアでした。それでは良き開発を〜