- 投稿日:2021-01-23T23:12:27+09:00
推し声優のラジオ番組を通知するプログラム組んだ
まえがき
皆さんは超A&G+というラジオ放送局をご存知でしょうか?
主に声優さんがパーソナリティを担当する番組を配信しているインターネットラジオです。作ろうと思ったきっかけ
在宅作業がデフォルトになったことにより、声優さんのラジオ番組を誰にも気兼ねなく聴ける環境になったので、これを機に推しの声優さんがパーソナリティを務めるラジオを聴くようになりました。
ただ、まだ声優ラジオを聴くのが習慣化されていないせいで、自分の好きな声優さんが超A&Gでラジオ番組をもっているかどうか、また持っていても、やっている時間を忘れてしまって聴く機会を逃していました。
「じゃあ録音すれば?」との声が聞こえてきそうで、実際、技術書典で本を買ったりして、録音プログラムを組んだりはしたのですが、録音したものを片っ端から聞いているのかというとそういうわけでもないので、録音したファイルがひたすらストレージの肥やしになってしまったのです。
どうやら、 『自分が作業しているタイミングで、好きな声優のラジオ番組があれば、BGMとしてかけたい』 というのが根本的な要望のようでした。
そんなわけで、実現する要件としては、 『自分の好きな声優さんがやっている番組があれば、ON AIRの一時間前に通知を送る』 というところとなりました。
システムの構成
こんな感じ。
- Google Apps Scriptにて、一時間に一回、その日に超A&G+で放送される番組一覧データをJSON形式でAPIを通して受け取ります。
- Google Spread Sheetに記載しておいた、自分の好きな声優やアニメのキーワードで、受信したJSONデータの中を探索して、該当するものがあるか調べていきます。
- 該当するものがあれば、Google Chatにて作成したChatRoomのWebHooks宛てに、「番組名・パーソナリティ・開始時間」を通知します。該当するものがなければ何もしません。また、なんらかの事情でAPIからデータを受け取れなかった場合は、「番組データの取得に失敗しました。」というエラーメッセージを通知します。
Google Chat(旧HangOuts Chat)を使って通知するのは、会社がGSuiteに登録していて、業務上のやりとりの多くをGoogle Chatを通して行うからです。
在宅作業時には常にChatを開いているので、ここに通知を飛ばしておけば、確認がもれることがありません。また、本当はスプレッドシートではなく、「HangOuts Chat API」を通してBotを作成し、Botに対して検索対象とする声優などのキーワードをCRUDしたかったのですが、アカウントが業務用のもので、HangOuts Chat APIを勝手に有効にすると、会社全体でこの声優ラジオ通知システムの存在を知ることができてしまい、オタバレする可能性があります。
いまのところ私は「アニメとか漫画とかラノベ好きだよー」程度の一般人としか認識されてないはずなので、そのイメージを守りたかったのです。
(実は水瀬いのりちゃんのファンクラブに入ってたり、部屋に数体のフィギュアが飾られてたり、週一で同人誌ショップに通って新作チェックするくらいにはオタ)また、HangOuts Chat Botを通して声優などのキーワードをCRUDする場合、どこかしらへのデータ保存用のサーバーが必要になってしまうのも、HangOuts Chat APIを使えないなと思った理由の一つです。
そんなわけでシステム実現の要件を整理すると、
- Google Chatを使って通知を行う
- 無料でできる
- 会社の人にこのシステムの存在をバレないようにする
といったところから、ありがちな上記のdraw.ioの通りのシステム構成をとりました。
通知のイメージ
画像はテストデータで出力したイメージです。
特にレイアウトに凝ったことはしていません。
単に推しの声優の番組が始まることが知れればいいので、
番組名・パーソナリティ・開始時間をテキストで通知します。実際のソース
githubにあります。
もし使ってくださる方がいれば、使ってみてください。
設定等はgithubのREADMEを見ていただければ。システム構成の実現方法
番組一覧データをJSON形式で受信するAPI
おそらく多くの方が、ここが一番気になっているのではなかろうかと。
公式かどうか存じてはいないのですが、探してみると存在はしていました。
@taittideさんという方が作成されていて、その過程もこの記事とかに書いていらっしゃいます。この記事を通して感謝を申し上げます。感謝…圧倒的感謝…
このAPIがなければ番組一覧取得用のAPIを自分で一から作ることになっていたので、ほんとありがとうございました。
どうかサービス終了しないでください…WebAPIとはなんぞや?という方に関しては、2021年2月号(今月号)のSoftware Designを購入してみるといいかもです。
イイ感じに説明してくれています。話を本題に戻しまして、このAPIでは以下のようなJSONデータを返してくれます。
[ { title: '千葉翔也のトゥー・ビー・ナイト', ft: '202101230000', to: '202101230057', pfm: '千葉翔也', dur: 57, isBroadcast: true, isRepeat: false }, { title: 'ファンキル・タガタメプレゼンツ 今泉Pの絶!つながるラジオ', ft: '202101230057', to: '202101230100', pfm: '今泉潤', dur: 3, isBroadcast: true, isRepeat: false }]これは実際にはデータの一部なのですが、要はオブジェクトの配列としてデータをくれるわけですね。
title: 番組名
ft: 開始日時
to: 終了日時
pfm: パーソナリティ
dur: 放送時間
isBroadCast: 配信中かどうか
isRepeat: 再放送かどうか今回は特に、titleとpfmに対して、スプレッドシートに登録しておいたキーワードを使ってindexOfをかけて、かつ現在時+1と+2の範囲に収まるftがあるか…って感じで自分の興味の対象となる番組があるかを判定しています。
現状、APIは無料かつこちらのスクリプトの速度も問題ないので、毎回APIにリクエストを投げてデータを取得し処理していますが、このどちらかの前提が崩れてくると、別途キャッシュ用のスプレッドシートとかを用意してあげないといけないな、とは考えています。
スプレッドシートからキーワードを読み取る
これに関しては多くの方が記事にしていらっしゃいますので、
ここで新たに説明することはしません。
代わりに参考になる記事を貼っておきます。あとがき
最後まで読んでいただきありがとうございました!
これでいろんな声優さんの番組を聴けるぞー
- 投稿日:2021-01-23T23:12:27+09:00
推し声優のラジオ番組をON AIR一時間前に通知する
まえがき
皆さんは超A&G+というラジオ放送局をご存知でしょうか?
主に声優さんがパーソナリティを担当する番組を配信しているインターネットラジオです。作ろうと思ったきっかけ
在宅作業がデフォルトになったことにより、声優さんのラジオ番組を誰にも気兼ねなく聴ける環境になったので、これを機に推しの声優さんがパーソナリティを務めるラジオを聴くようになりました。
ただ、まだ声優ラジオを聴くのが習慣化されていないせいで、自分の好きな声優さんが超A&Gでラジオ番組をもっているかどうか、また持っていても、やっている時間を忘れてしまって聴く機会を逃していました。
「じゃあ録音すれば?」との声が聞こえてきそうで、実際、技術書典で本を買ったりして、録音プログラムを組んだりはしたのですが、録音したものを片っ端から聞いているのかというとそういうわけでもないので、録音したファイルがひたすらストレージの肥やしになってしまったのです。
どうやら、 『自分が作業しているタイミングで、好きな声優のラジオ番組があれば、BGMとしてかけたい』 というのが根本的な要望のようでした。
そんなわけで、実現する要件としては、 『自分の好きな声優さんがやっている番組があれば、ON AIRの一時間前に通知を送る』 というところとなりました。
システムの構成
こんな感じ。
- Google Apps Scriptにて、一時間に一回、その日に超A&G+で放送される番組一覧データをJSON形式でAPIを通して受け取ります。
- Google Spread Sheetに記載しておいた、自分の好きな声優やアニメのキーワードで、受信したJSONデータの中を探索して、該当するものがあるか調べていきます。
- 該当するものがあれば、Google Chatにて作成したChatRoomのWebHooks宛てに、「番組名・パーソナリティ・開始時間」を通知します。該当するものがなければ何もしません。また、なんらかの事情でAPIからデータを受け取れなかった場合は、「番組データの取得に失敗しました。」というエラーメッセージを通知します。
Google Chat(旧HangOuts Chat)を使って通知するのは、会社がGSuiteに登録していて、業務上のやりとりの多くをGoogle Chatを通して行うからです。
在宅作業時には常にChatを開いているので、ここに通知を飛ばしておけば、確認がもれることがありません。また、本当はスプレッドシートではなく、「HangOuts Chat API」を通してBotを作成し、Botに対して検索対象とする声優などのキーワードをCRUDしたかったのですが、アカウントが業務用のもので、HangOuts Chat APIを勝手に有効にすると、会社全体でこの声優ラジオ通知システムの存在を知ることができてしまい、オタバレする可能性があります。
いまのところ私は「アニメとか漫画とかラノベ好きだよー」程度の一般人としか認識されてないはずなので、そのイメージを守りたかったのです。
(実は水瀬いのりちゃんのファンクラブに入ってたり、部屋に数体のフィギュアが飾られてたり、週一で同人誌ショップに通って新作チェックするくらいにはオタ)また、HangOuts Chat Botを通して声優などのキーワードをCRUDする場合、どこかしらへのデータ保存用のサーバーが必要になってしまうのも、HangOuts Chat APIを使えないなと思った理由の一つです。
そんなわけでシステム実現の要件を整理すると、
- Google Chatを使って通知を行う
- 無料でできる
- 会社の人にこのシステムの存在をバレないようにする
といったところから、ありがちな上記のdraw.ioの通りのシステム構成をとりました。
通知のイメージ
画像はテストデータで出力したイメージです。
特にレイアウトに凝ったことはしていません。
単に推しの声優の番組が始まることが知れればいいので、
番組名・パーソナリティ・開始時間をテキストで通知します。実際のソース
githubにあります。
もし使ってくださる方がいれば、使ってみてください。
設定等はgithubのREADMEを見ていただければ。システム構成の実現方法
番組一覧データをJSON形式で受信するAPI
おそらく多くの方が、ここが一番気になっているのではなかろうかと。
公式かどうか存じてはいないのですが、探してみると存在はしていました。
@taittideさんという方が作成されていて、その過程もこの記事とかに書いていらっしゃいます。この記事を通して感謝を申し上げます。感謝…圧倒的感謝…
このAPIがなければ番組一覧取得用のAPIを自分で一から作ることになっていたので、ほんとありがとうございました。
どうかサービス終了しないでください…WebAPIとはなんぞや?という方に関しては、2021年2月号(今月号)のSoftware Designを購入してみるといいかもです。
イイ感じに説明してくれています。話を本題に戻しまして、このAPIでは以下のようなJSONデータを返してくれます。
[ { title: '千葉翔也のトゥー・ビー・ナイト', ft: '202101230000', to: '202101230057', pfm: '千葉翔也', dur: 57, isBroadcast: true, isRepeat: false }, { title: 'ファンキル・タガタメプレゼンツ 今泉Pの絶!つながるラジオ', ft: '202101230057', to: '202101230100', pfm: '今泉潤', dur: 3, isBroadcast: true, isRepeat: false }]これは実際にはデータの一部なのですが、要はオブジェクトの配列としてデータをくれるわけですね。
title: 番組名
ft: 開始日時
to: 終了日時
pfm: パーソナリティ
dur: 放送時間
isBroadCast: 配信中かどうか
isRepeat: 再放送かどうか今回は特に、titleとpfmに対して、スプレッドシートに登録しておいたキーワードを使ってindexOfをかけて、かつ現在時+1と+2の範囲に収まるftがあるか…って感じで自分の興味の対象となる番組があるかを判定しています。
現状、APIは無料かつこちらのスクリプトの速度も問題ないので、毎回APIにリクエストを投げてデータを取得し処理していますが、このどちらかの前提が崩れてくると、別途キャッシュ用のスプレッドシートとかを用意してあげないといけないな、とは考えています。
スプレッドシートからキーワードを読み取る
これに関しては多くの方が記事にしていらっしゃいますので、
ここで新たに説明することはしません。
代わりに参考になる記事を貼っておきます。あとがき
最後まで読んでいただきありがとうございました!
これでいろんな声優さんの番組を聴けるぞー
- 投稿日:2021-01-23T23:12:22+09:00
Vim & VSCodeのフォーマッタを自作する(分析SQLを例に)
概要
こちらの「分析SQLスタイルガイド」を拝見し、SQLのフォーマッタには需要があると感じて早速作り始めてみた。とりあえず動くようになったので、これまでの一部始終を記事にする。具体的には、以下の内容を扱う。
- Prettierという既存のフォーマッタをコマンドやエディタから利用する方法
- Prettierのプラグインとして任意の言語のフォーマッタを実装する方法(分析SQLを題材にする)
普段から分析SQLを書くがフォーマッタは使ったことがないという人は、まずこの後のPrettierの紹介を読んでほしい。きっと使いたくなるはず。
▼ちょっと画質悪いけど、1月23日現在の開発進捗。とりあえず動く。
Prettierとは
Prettier
デフォルトではJavaScript・HTML・CSSなど1に対応したコードフォーマッタ。これを使うと、コマンド一発で以下のようにコードをフォーマットできる。不自然に1行に並べられたコードがきれいに改行・インデントされ、セミコロンが挿入されていることがわかる。
App.js// フォーマット前 const http = require("http");const server = http.createServer((req, res) => {res.end("hello world")});server.listen(3000) // フォーマット後 const http = require("http"); const server = http.createServer((req, res) => { res.end("hello world"); }); server.listen(3000);これを導入すると、例えば以下のようなメリットがある。
- 最小限の労力でチーム内のコーディングスタイルを統一できる
- コードディングスタイルではなくコードの内容に意識と時間を集中できる
分析SQLの文脈だと、例えばチーム内に予約語大文字派と小文字派が共存していても、共有前にPrettierを使うだけでスタイルを揃えることができる。これは時間の節約にも貢献するだろう2。
Prettierプラグイン
Prettierはデフォルトで対応していない言語にも、プラグインで対応できる。SQLもこのパターンで、prettier-plugin-pgというプラグインがあるが、実用段階ではないらしい。それなら最低限機能するプラグインを自分で作ってしまおうというのがこの記事のスタンス。ちなみに利用可能なプラグインはここで紹介されている。
基本的な使い方
インストール
以下のコマンドでインストールできる。プラグインを利用する場合は一緒にインストールするとよい。
-g
オプションでグローバルにインストールすることもできるが、あまり推奨されていないようだ。npm install --save-dev --save-exact prettierコマンドラインからの利用
prettierをインストールしたプロジェクト内で以下のコマンドを実行する。対象は
App.js
のようなファイル名でもよいし、.
でまとめて指定してもよい。私はまだ試していないが、ドキュメントによるとcommit前に自動で実行することもできるらしい。npx prettier --write App.js
Vim (Neovim) からの利用
vim-prettierが便利。vim-plugを利用しているなら、以下を.vimrc (init.vim)に追記して
:PlugInstall
を実行。init.vimcall plug#begin('~/.vim/plugged') " ... other plugins Plug 'prettier/vim-prettier', { 'do': 'yarn install' } " ... other plugins call plug#end()
:Prettier
というのがフォーマットを行うコマンドで、デフォルトだと<leader>p
に割り当てられる。:PrettierAsync
コマンドの方が快適なので、基本こちらを利用するとよさそう。
ちなみにvim-prettierのインストール時にPrettierもインストールされるので、Prettierプラグインが必要ないなら前段のnpm install ...
を省略できる3。VSCodeからの利用
prettier-vscodeを使う。まずはExtensionsから
Prettier - Code formatter
を検索してインストールする。
Prettierを利用することを明示するため、必要に応じてsetting.jsonに以下を記入しておく。
setting.json{ "editor.defaultFormatter": "esbenp.prettier-vscode" }ここまでで準備は完了。
CMD + Shift + P
でコマンドパレットを開き、Format Document
を選択すればフォーマットを実行できる。
ちなみにvim-prettierと同様、prettier-vscodeについても以下のことがいえる。ちなみにvim-prettierのインストール時にprettierもインストールされるので、Prettierプラグインが必要ないなら前段の
npm install ...
を省略できる3。Prettierプラグインの実装(分析SQLを例に)
ここからPrettierプラグインの実装方法を紹介する。Prettierのチュートリアルを参考にしているが、さらに簡単な内容に限って記載する。なお、この記事のコードは全てGitHubにあげている。
準備
npmパッケージとして実装していく。まずは以下のコマンドでプロジェクトの初期化と依存パッケージのインストールを行う。
npm init -y npm install --save prettier node-sql-parserそしてindex.jsという名称で以下のファイルを作成する。これはチュートリアルで紹介されたコードを参考にしたもので、テンプレだと思ってほしい。printSQL関数のみ後ほど編集する。
index.jsconst { Parser } = require("node-sql-parser"); const parser = new Parser(); const { doc: { builders: { concat, hardline, group, indent, softline, join, line }, }, util, } = require("prettier"); const languages = [ { extensions: [".sql"], name: "sql", parsers: ["sql-parse"], }, ]; const parsers = { "sql-parse": { parse: (text) => parser.astify(text), astFormat: "sql-ast", }, }; function printSQL(path, options, print) { const node = path.getValue(); if (Array.isArray(node)) { return concat(path.map(print)); } switch (node.type) { default: return ""; } } const printers = { "sql-ast": { print: printSQL, }, }; module.exports = { languages, parsers, printers, };parserの実装
Prettierでフォーマットを行う際、テキストは一時的にAST(AbstractSyntaxTree、抽象構文木)に変換される。この役割を担うのがparserである。今回は自前で実装せずに、node-sql-parserを利用する。以下に簡単な使い方と、出力されるASTを示す。
const { Parser } = require("node-sql-parser"); const parser = new Parser(); const ast = parser.astify("select c1,c2,c3 from data") console.log(ast) /* { with: null, type: 'select', options: null, distinct: null, columns: [ { expr: [Object], as: null }, { expr: [Object], as: null }, { expr: [Object], as: null } ], from: [ { db: null, table: 'data', as: null } ], where: null, groupby: null, having: null, orderby: null, limit: null, for_update: null } */printerの実装
ASTをフォーマット済みのコードに変換するのがprinterの役割。まずはindex.js内のprintSQL関数を再掲する。ASTに対してこの関数を再帰的に適用していくことになる。今はまだ何も出力しない状態だが、処理の流れをコメントで解説しておく。
index.js// printSQLのみ再掲 function printSQL(path, options, print) { const node = path.getValue(); // pathからASTを抽出 if (Array.isArray(node)) { // nodeが配列の場合、各要素にprintSQL関数を再帰的に適用 return concat(path.map(print)); // path.map(print)が再帰的な呼び出しを表す。戻り値が配列だからconcatで結合 } switch (node.type) { // node.typeに応じてフォーマット処理をして返す default: return ""; } }単純なselect文のフォーマットに対応するなら以下のようになる。詳細はコメントで解説しているので見てほしい。難しいのは
path.call(print, "xxx")
path.map(print, "xxx")
だと思うが、printSQL関数を再帰的に適用する定型文のようなもの。index.js// printSQL修正版 function printSQL(path, options, print) { const node = path.getValue(); if (Array.isArray(node)) { return concat(path.map(print)); } if (node.expr) { // nodeが {expr: {type: "xxx"}} のような形式に対応 return path.call(print, "expr"); // nodeからexprプロパティのみ抽出してprintSQLを適用。path.mapと違って戻り値は文字列(配列ではない) } switch (node.type) { case "select": // nodeが {type: "select"} の場合 return concat([ "SELECT", indent( // indent()の内側はインデントされる concat([ hardline, // "SELECT"直後の改行 join(concat([hardline, ","]), path.map(print, "columns")), // nodeからcolumnsプロパティを抽出しprintSQLを適用。各要素は改行とカンマで結合 ]) ), hardline, // 改行 "FROM", indent(concat(node.from.map((x) => concat([hardline, x.table])))), ]); case "column_ref": // nodeが {type: "column_ref"} の場合 return node.column; default: return ""; } }まだSELCT句とFROM句のみの単純なSQLしか処理できないが、これでいったん動く。ここまで理解できたら、あとはASTの構造をよく見て処理を充実させるだけ!
動作確認
実装の解説はここまでにして、最後に動作確認を行う。プロジェクトのルートディレクトリ(package.jsonがあるところ)で以下のコードを実行すると、きれいに改行・インデントされ、予約語を大文字に変換したSQLが表示されるはず。
check.jsconst prettier = require("prettier"); const format = (code) => { const res = prettier.format(code, { parser: "sql-parse", plugins: ["."], }); return res; }; console.log(format("select c1,c2,c3 from data")); /* SELECT c1 ,c2 ,c3 FROM data */公開
npm publish
でアップロードしたら、既存のPrettierプラグインと同じように利用できる。Prettierプラグインとして認識されるには、パッケージの名称が@prettier/plugin-
prettier-plugin-
@<scope>/prettier-plugin-
のいずれかで始まる必要があるので、package.jsonを適切に編集しておくこと。最後に
この記事ではSELECT句とFROM句のみのSQLに対応したフォーマッタを作成した。もちろんこれでは業務に使えないので、より実用的なSQLフォーマッタを現在作成中(主にBigQuery用を想定、GitHubはこちら)。進捗としてはある程度複雑なSQLにも対応しつつあるが、node-sql-parserがBigQuery独自の文法には弱かったり、コメントアウトを消してしまったりと実は課題も多い状況にある。parserから再検討するので時間はかかりそうだが、せっかくなので最後まで頑張ってみる(LGTMとかで応援してくれたらもっと頑張る)
- 投稿日:2021-01-23T22:00:41+09:00
【Java】java.lang.NullPointerExceptionを避ける方法
プログラミング勉強日記
2021年1月23日
Javaでコードを書いているときに、よく出てくるNullPointerExcptionの意味と、NullPointerExcptionにならないようにする方法をまとめる。java.lang.NullPointerExceptionRequest processing failed; nested exception is java.lang.NullPointerExceptionNullPointerExceptionとは
参照型の変数の値にnullが格納されているとき、それを参照しようとしたときには発生する例外である。
NullPointerException(ナル・ポインタ・エクセプション、ヌル-)は、プログラミング言語Javaにおける例外の一つである。
null値(定義されていない値)の参照型変数を参照しようとした時に発生する。NullPointerExceptionは実行時例外と呼ばれるjava.lang.RuntimeException クラスのサブクラスであるため、try-catch節による例外処理を書かなくてもコンパイルエラーは発生しない。
(引用:https://ja.wikipedia.org/wiki/NullPointerException)なので、以下のようなコードだとNullPointerExcptionが発生する。
class Sample{ public static void main(String[] args) { Integer i = null; i.toString(); } }NullPointerExcptionを避ける方法
基本的には、参照型変数がnullかどうか判定すればいい。
先ほどのコードではNullPointerExcptionが発生したが、参照型変数がnullの場合は処理をしないというコードに書きかえることでNullPointerExcptionをなくせる。class Sample{ public static void main(String[] args) { Integer i = null; // i がnullの場合、処理をしない if(i == null) { return; } i.toString(); } }しかし、コード量が増えるほど判定は大変になるので対策方法を示す。
- null以外の値で変数を初期化する
- 配列は空で初期化する
- 戻り値にnullを返さない
この3つを意識することで、NullPointerExcptionあ発生しにくくなると思う。
参考文献
- 投稿日:2021-01-23T21:34:19+09:00
Axiosで外部ファイルをロードしてFileにする / Data URI schemeをFileにする
- 外部リソースをAxiosでロードし
File
オブジェクトに変換する- Data URI schemeを
File
オブジェクトに変換するuriToFile関数import axios from 'axios' function uriToFile(uri, fileName) { if (!uri) return Promise.resolve(null) return uri.startsWith('data:') ? convertDataUriToFile(uri, fileName) : loadUriToFile(uri, fileName) } function convertDataUriToFile(dataUri, fileName) { const byteString = atob(dataUri.split(',')[1]) const mimeType = dataUri.match(/(:)([a-z/]+)(;)/)[2] const length = byteString.length const content = new Uint8Array(length) for (let i = 0; length > i; i++) { content[i] = byteString.charCodeAt(i) } return Promise.resolve( new File([content], fileName, { type: mimeType, }) ) } async function loadUriToFile(uri, fileName) { const response = await axios.get(uri, { responseType: 'blob', }) return new File([response.data], fileName, { type: response.data.type, }) }使い方uriToFile('https://example.com/example.png') .then(file => ...) uriToFile('data:image/png;base64,iVBORw0KGgoAAA...') .then(file => ...)
- 投稿日:2021-01-23T19:51:34+09:00
Webサービスにおけるダークモード対応方法
先日、携わらせていただいているSpotlightというサービスのデザインリニューアルをしました。
その際にダークモードに対応したので、それの知見共有です。ダークモードの対応方法
CSSでの対応です。
こんな感じで対応したい部分をメディアクエリの中に書き込みます。@media (prefers-color-scheme: dark) { body { background-color: #000; } }それだけ!
ダークモード、ライトモードの切り替え方法
これが苦労しました。
Spotlightでは以下仕様でダークモードに対応しています。未ログインのユーザー → 端末のデフォルト設定
ログイン済かつサービス内でテーマ設定していないユーザー → 端末のデフォルト設定
ログイン済かつサービス内でテーマ設定しているユーザー → 設定に合わせるコードにするとこんな感じ。
document.documentElement.setAttribute('theme', getTheme()); function getTheme() { const userTheme = localStorage.getItem('user'); if (userTheme) { return userTheme; } const isDark = matchMedia('(prefers-color-scheme: dark)').matches; return isDark ? 'dark' : 'light'; }@media (prefers-color-scheme: light) { :root { ... } /* 端末設定がライトテーマ */ :root[theme="dark"] { ... } /* 端末設定がライトテーマかつサービス設定がダークテーマ */ } @media (prefers-color-scheme: dark) { :root { ... } /* 端末設定がダークテーマ */ :root[theme="light"] { ... } /* 端末設定がダークテーマかつサービス設定がライトテーマ */ }
:root
についてはこの記事みてくれると意味がわかるかとサービス内でテーマ設定をするとlocalStorageでデータが保持されます。
localStorageの他にも色々ありますが、今回はユーザーの使用する端末ごとに設定変更ができるよう、localStorageにしています。HTMLに
<html theme="">
が入って、切り替わる仕組みなのでCSSではコメントアウトのように対応したい部分のCSSを書き込みます。さらに、テーマ設定でテーマを切り替えなきゃいけないのでそれも実装します
SpotlightはToggleボタンをClass名切り替えで実装しているので、ちょっとややこしいため今回はボタンクリックしたら切り替わるようにします<button onclick ="changeTheme(event)"></button>function changeTheme(ev) { const currentTheme = document.documentElement.getAttribute('theme'); const newTheme = currentTheme == 'dark' ? 'light' : 'dark' document.documentElement.setAttribute('theme', newTheme); localStorage.setItem('user', newTheme); }実際どんな感じで動作するかはぜひアカウント登録して確かめてください。
ダークモードの色について
最近はいろんなサービスでダークモードが採用されてるので、Spotlightはどんな感じでいこうかな〜と他サービスを調査したのでこれからダークモードを採用される方はぜひ参考にしてください。
iOS
背景: #000000 サブ背景: #1C1C1E 文字: #FFFFFF サブ文字: #EBEBF5 60%Android
背景: #121212 サブ背景: #121212 + #FFFFFF 12% 文字: #FFFFFF サブ文字: #FFFFFF 60%ダークブルー 背景: #15202B サブ背景: #15212B 文字: #FFFFFF サブ文字: #8899A6 ブラック 背景: #000000 サブ背景: #15181C 文字: #D9D9D9 サブ文字: #6E767D背景: #18191A サブ背景: #242526 文字: #E4E6EB サブ文字: #B0B3B8Spotlight
背景: #000000 サブ背景: #222222 文字: #FFFFFF サブ文字: #999999文字と背景色のコントラスト比の数値は、19〜14くらいがチラつかずに見やすい文字になると思います。
気になる箇所あれば連絡ください。
- 投稿日:2021-01-23T18:42:56+09:00
【Vue.js基礎第1回】環境構築・ディレクティブ
はじめに
Vue.jsの基礎について複数回に渡ってまとめていきます。今回は「環境構築」と「ディレクティブ」についてです。動作確認済みのコードも記述しています。※内容は初学者向けです。
準備
今回はCDNを用いて導入。Vueの公式ホームページより最新のCDNを取得。bodyタグの直前に記述。別のファイルにJSを記述する場合は、CDNの後に挿入する。
https://jp.vuejs.org/v2/guide/installation.html#CDNcdn.html<body> <div id="app"> //本記事ではここにVueで操作したい内容を記述 </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script src="app.js"></script> </body> </html>
new Vue
でインスタンスを生成し、html内のVueを使用する場所をel要素で指定する。vue.jsnew Vue({ el:'#app' })ディレクティブ
v-
という接頭辞がついたVue.jsの属性のこと。DOMに対して動作を適用することができる。参考:https://rightcode.co.jp/blog/information-technology/vue-js-introduction-directive
①v-bind
タグの属性を動的に設定できる。
v-bind:<属性の名前="属性に代入される変数名">使用例:
v-bind.html<div id="app"> <a v-bind:href="url">URL</a> </div>v-bind.jsnew Vue({ el:'#app', data: { url : 'http://localhost' } })
v-bind
は省略できる。v-bind2.html<a :href="url">URL</a>②v-on
イベントのトリガーを呼び出す関数を定義する。
v-on:<イベント名>="<呼び出す関数名>"参考:v-onの後ろに置くことができるDOMイベント
https://developer.mozilla.org/ja/docs/Web/Events引数を取らない時
メソッド名に()は不要。あっても問題ない。
v-on.html<div id="app"> <p>カウント数:{{ counter }}</p> <button v-on:click="countUp">カウントアップ</button> </div>v-on.jsnew Vue({ el:'#app', data: { counter: 0 }, methods:{ countUp:function(){ this.counter += 1; } } })引数をとる時
引数を複数渡すこともできる。下記例は引数に10を渡して10倍ずつカウントアップしている。
v-on2.html<div id="app"> <p>カウント数:{{ counter }}</p> <button v-on:click="countUp(10)">カウントアップ</button> </div>v-on2.jsnew Vue({ el:'#app', data: { counter: 0 }, methods:{ countUp:function(times){ this.counter += 1 * times; } } })
v-on:
は@で省略可能v-on3.html<div id="app"> <p>カウント数:{{ counter }}</p> <button @click="countUp">カウントアップ</button> </div>③v-model
双方向バインディングを可能にするため、テキストボックスの文字列を変更すると、連動してのVue側の値も変更される。
双方向バインディング:
スクリプト側から値の設定とブラウザからの入力のどちらからでも双方向で値を更新できる仕組み。
ブラウザから入力 <==> 変数 a <==> JavaScriptから更新
参考:https://ameblo.jp/forefrontier/entry-12330323176.html
※バインディングは結びつけるの意。使用例:
v-model.html<div id="app"> <input type="text" v-model="message"> <h1>{{ message }}</h1> </div>v-model.jsnew Vue({ el:'#app', data: { message: 'Hello、JS!' } })入力フォームに「Hello、JS!」と入力すると、データが変更される↓
④v-if
条件に応じてhtml要素を動的に変更する。
使用例:FizzBuzzをカウントアップで表示
<div id="app"> <p v-if="FizzBuzz">FizzBuzz</p> <p v-else-if="Fizz">Fizz</p> <p v-else-if="Buzz">Buzz</p> <p v-else>{{ count }}</p> <button @click="countUp">カウントアップ</button> </div>new Vue({ el:'#app', data: { count:1 }, methods: { countUp:function(){ this.count += 1; } }, computed: { FizzBuzz: function() { return this.count %3 == 0 && this.count % 5 == 0; }, Fizz : function() { return this.count %3 == 0; }, Buzz : function() { return this.count %5 == 0; } } })表示「FizzBuzz」
表示「Fizz」
表示「else」
⑤v-for
オブジェクトや配列の中身を表示する。
v-for = "<配列やオブジェクト名> in <配列やオブジェクトの複数形>"使用例:
v-for.html<div id="app"> //配列の場合 <ul> <li v-for="color in colors">{{ color }}</li> </ul> //オブジェクトの場合 <ul> <li v-for="value in object">{{ value }}</li> </ul> </div>v-for.jsnew Vue({ el:'#app', data: { colors: ['赤','青','緑'], objects: { name: 'Evan You', age: 35, birthplace:'China' } } })オブジェクトや配列は引数をとることができる。
v-for2.js<div id="app"> <ul> <li v-for="(value,key,index) in objects">({{ index }}){{ key }}-{{value }}</li> </ul> </div>最後に
Vueのディレクティブについてまとめました。複数回に渡ってVueの基礎をまとめていきます。
(もしこの記事に誤りがありましたらご教授いただけると幸いです。)
- 投稿日:2021-01-23T18:16:58+09:00
初めて作ったChrome拡張で使用した汎用性が高いと思われるコード
どんな拡張?
Firefoxアドオンの「定期的にチェック」がリニューアルに伴い消えてしまい、作者も活動を休止しているようなのでChrome拡張に移植しました。
定期的にチェックしたいページを1日毎、1週間毎というように定期的に表示します。
半年ごとに表示するなどしてリマインダー代わりに使うこともできます。ダウンロードはこちら→ https://chrome.google.com/webstore/detail/check-periodically/fpmgbnmhlmbknpkdlgdmockhiojelddp
基本コード
Chrome拡張開発入門はすでにたくさんあるのでここではカンタンな拡張でも使うような基本的なコードを紹介します。
データ操作
manifest.jsonの
permission
に"storage"
を追加する必要があります。"permissions": [ "storage" ],データ初期化
chrome.runtime.onInstalled.addListener((details) => { if(details.reason == "install"){ //Init data chrome.storage.sync.set({'pages': {}}) chrome.storage.sync.set({"last_check_time": new Date().getTime()}) chrome.storage.sync.set({"menu": [1,3,7,30,180,365]}) } });
chrome.runtime.onInstalled.addListener()
を使うとインストールした最初の1回だけコードを実行させることができます。データ読み取り
chrome.storage.sync.get(['pages', "last_check_time"], (result) => { for(const [url, detail] of Object.entries(result.pages)){ //...処理 } })データの読み取りには
chrome.storage.sync.get([変数名1, 変数名2, ....], (result) =>{処理})
を使います。このとき、2番目の引数を関数にし、そこに引数をつけて読み取るという形にします。取得したデータはこの関数内部でしか使えないので、何か恒常的に保持しておきたい変数があったら関数外部でlet hoge
のように宣言し関数内部でhoge = xxx
と代入しなければいけません。この辺はjavascriptに慣れてないとややこしいかもしれません。データ保存
chrome.storage.sync.set({"pages":result.pages})保存したいデータ(ここではresult.pages)とその変数名(ここではpages)をオブジェクト型のようにして保存します。
現在のタブを取得
manifest.jsonの
permission
に"tabs", "activeTab"
を追加する必要があります。"permissions": [ "tabs", "activeTab" ],
chrome.tabs.query
はいろんなタブを検索できますが、現在のタブを取得するには以下のようにします。chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { const currentTab = tabs[0]; // there will be only one in this array const url = currentTab.url; });現在のタブの変更を検出
chrome.tabs.onActivated.addListener((activeInfo) => { chrome.tabs.get(activeInfo.tabId, (tab) =>{ console.log(tab.url) }) })このコードではタブが切り替わるたびに現在アクティブなタブの情報が入ったactiveInfoを取得でき、
chrome.tabs.get(activeInfo.tabId, (tab)=>{処理})
で現在のタブを取得できます。
- 投稿日:2021-01-23T18:16:58+09:00
Chrome拡張開発の基本コードTips
基本コード
Chrome拡張開発入門はすでにたくさんあるのでここではカンタンな拡張でも使うような基本的なコードを紹介します。
データ操作
manifest.jsonの
permission
に"storage"
を追加する必要があります。"permissions": [ "storage" ],データ初期化
chrome.runtime.onInstalled.addListener((details) => { if(details.reason == "install"){ //Init data chrome.storage.sync.set({'pages': {}}) chrome.storage.sync.set({"last_check_time": new Date().getTime()}) chrome.storage.sync.set({"menu": [1,3,7,30,180,365]}) } });
chrome.runtime.onInstalled.addListener()
を使うとインストールした最初の1回だけコードを実行させることができます。データ読み取り
chrome.storage.sync.get(['pages', "last_check_time"], (result) => { for(const [url, detail] of Object.entries(result.pages)){ //...処理 } })データの読み取りには
chrome.storage.sync.get([変数名1, 変数名2, ....], (result) =>{処理})
を使います。このとき、2番目の引数を関数にし、そこに引数をつけて読み取るという形にします。取得したデータはこの関数内部でしか使えないので、何か恒常的に保持しておきたい変数があったら関数外部でlet hoge
のように宣言し関数内部でhoge = xxx
と代入しなければいけません。この辺はjavascriptに慣れてないとややこしいかもしれません。データ保存
chrome.storage.sync.set({"pages":result.pages})保存したいデータ(ここではresult.pages)とその変数名(ここではpages)をオブジェクト型のようにして保存します。
現在のタブを取得
manifest.jsonの
permission
に"tabs", "activeTab"
を追加する必要があります。"permissions": [ "tabs", "activeTab" ],
chrome.tabs.query
はいろんなタブを検索できますが、現在のタブを取得するには以下のようにします。chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { const currentTab = tabs[0]; // there will be only one in this array const url = currentTab.url; });現在のタブの変更を検出
chrome.tabs.onActivated.addListener((activeInfo) => { chrome.tabs.get(activeInfo.tabId, (tab) =>{ console.log(tab.url) }) })このコードではタブが切り替わるたびに現在アクティブなタブの情報が入ったactiveInfoを取得でき、
chrome.tabs.get(activeInfo.tabId, (tab)=>{処理})
で現在のタブを取得できます。作った拡張
Firefoxアドオンの「定期的にチェック」がリニューアルに伴い消えてしまい、作者も活動を休止しているようなのでChrome拡張に移植しました。
定期的にチェックしたいページを1日毎、1週間毎というように定期的に表示します。
半年ごとに表示するなどしてリマインダー代わりに使うこともできます。ダウンロードはこちら→ https://chrome.google.com/webstore/detail/check-periodically/fpmgbnmhlmbknpkdlgdmockhiojelddp
- 投稿日:2021-01-23T17:56:27+09:00
大なり(>)、大なりイコール(>=)使わないプログラミングのすすめ
大小比較の演算をするとき、大なり(>)、大なりイコール(>=)を使わずに、小なり(<)、小なりイコール(<=)を使いましょう。
JavaScriptを例にしていますがどの言語でも使えるテクニックです。例1
たとえば変数
hoge
が1以上100未満か判定する条件を例にします。(JavaScript)let hoge = 20; // good if (1 <= hoge && hoge < 100) {} // bad if (hoge >= 1 && hoge < 100) {}goodとbad、どちらもテストには合格するコードですが、どちらが読みやすいでしょうか?
goodの方が、1 <= hoge < 100
と読むことができるので、読みやすく感じるのではないでしょうか。例2
変数
fuga
が50未満かつ変数piyo
が30以上かを判定するコードを例にします。(JavaScript)let fuga = 20; let piyo = 30; // good if (fuga < 50 && 30 <= piyo) {} // bad if (50 > fuga && 30 <= piyo) {}大なりと小なりを織り交ぜるより、小なりまたは小なりイコールだけを利用しているほうが、条件式の理解がスッとできる。と感じませんか?(あんまりでしょうか…?)
小なり(<)、小なりイコール(<=)だけ使うメリット
- 可読性が良い。
- 大なりと小なりのどっちを使うか悩まなくなるのでコーディングが数秒早くなる。
- 理解しやすいコードが書ける=バグりにくいコードが書けるようになる。
大なり(>)、大なりイコール(>=)に統一じゃダメ?
それでもOKと思いますが一般的には小なり(<)、小なりイコール(<=)を勧めている規約が多い気がするので。
細かいこと気にしすぎと思った方
ここまで気にしながらプログラミングする人は少数かもしれません。プロジェクトのコード規約にもここまで書いていないかもしれません。
しかし自分の中でルールを作ることで統一感があり、悩まない、ブレないコードが書けるようになるのではないかと思います。
書く時間より読まれる時間の方が多いのがプログラムなので、細かいことでも気にしていきましょう。
- 投稿日:2021-01-23T17:50:57+09:00
Vue.jsの代わりにAlpine.jsでデータバインディング
はじめに
最近、Alpine.jsというライブラリを知りました。
Vue.jsに影響を受けているようで書き心地は似ているのですが、データバインディングに特化していてVue.jsよりも軽量なので、SPA用途でなくデータバインディング用途でVue.jsを使用しているという場合は、代替になり得ると感じました。Vue.jsと比較すると以下の点でメリットがあると感じます。
- 軽量
- データオブジェクトをPOJOで定義できるので、thisのコンテキストが明確
試してみる
APIがシンプルかつ日本語ドキュメントもあるので、基本的にはそれを見てもらえればと思いますが、Vue.jsの雰囲気で書こうとした場合にいくつか引っかかったので、その部分を共有出来たらと思います。
日本語ドキュメント:https://github.com/alpinejs/alpine/blob/master/README.ja.mdというのもAlpine.jsは「HTMLに属性値としてJavaScriptが書ける」ことを強みにしており、属性値で書くパターンとscriptタグで分けて書くパターンが、ドキュメント内に混在しているんですね。
今回はscriptタグで分けて書く場合にフォーカスを絞って、サンプルを紹介できたらと思います。
サンプルコード
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Alpine.js Sample</title> <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script> <style> /* Alpine.jsの初期化が終わるとx-cloak属性が削除される。つまり、x-cloak属性にdisplay:noneを 定義すると、x-cloak属性を持つタグは初期化が終わるまで非表示になり、画面のちらつきを抑えられる。 */ [x-cloak] { display: none } </style> </head> <body> <!-- データバインディングしたいタグのx-data属性でデータオブジェクトを返却する関数を実行する --> <div x-data="defineComponent()" x-init="initialize" x-cloak> <!-- Vue.jsと違いinnerTextの定義に {{ hoge }} が使えないため、x-text属性に定義する --> <h1 x-text="`Hello, ${fullname}!`"></h1> <h3 class="js_subtitle"></h3> <p x-text="`2way-binding: ${inputValue}`"></p> <!-- x-on:eventは@eventで省略可能 preventなどのmodifierも使用できる --> <form @submit.prevent="handleSubmit"> <!-- x-modelによる双方向バインディングも使用可能 --> <input type="text" x-model="inputValue"> <input type="submit" value="Submit"> </form> </div> <script> const defineComponent = () => ({ // プロパティはそのままリアクティブになる // Vue.jsにおけるdataオブジェクトに相当 firstName: 'Taro', lastName: 'Tanaka', inputValue: '', // Vue.jsにおけるcomputedオブジェクトに相当 get fullname() { return `${this.firstName} ${this.lastName}`; }, // Vue.jsにおけるmethodsオブジェクトは、そのままメソッドとして書ける initialize() { // x-initでinitializeを指定しているので、このメソッドがマウント後に実行される console.log('Initialize'); // コンポーネント内のDOMを掴みたいときはthis.$elで参照できる this.$el.querySelector('.js_subtitle').textContent = 'This is manual binding.'; // プロパティの変更を監視したい場合は、this.$watchのコールバックで定義できる // Vue.jsのwatchオブジェクトに相当 this.$watch('inputValue', (currentValue) => { console.log(`updated inputValue: ${currentValue}`); }); }, handleSubmit() { console.log(`submited with inputValue: ${this.inputValue}`); this.inputValue = ''; } }); </script> </body> </html>x-ifやx-forをtemplateタグ内に書く必要があったりと、Vue.jsと微妙に違う点がいくつかありますが、この例をベースにドキュメントを読み進んで頂ければ、理解の助けになるかと思います。
良いと思った点
Vue.jsと違いデータオブジェクトがプレーンなので、thisのコンテキストをエディタが推論可能になります。
今回はVSCodeを使用して書きましたが、拡張機能の追加なしにthisのプロパティやメソッドが推論されるのは良いですね。
メソッドがそのまま定義できますし、ゲッターを使用したcomputedの処理も直感的に感じます。今後、画面の一部でデータバインディングを使用したくなった際は、積極的に採用してみたいですね。
- 投稿日:2021-01-23T17:29:00+09:00
コメントは書かなくていいよ
どんな言語にも共通することですが、コメントは基本的に書かなくていいんです。
プログラミングの入門書にはよく「わかりやすくコメントを書きましょう」と書いてありますよね。
それを鵜呑みにした初心者プログラマーはこんなコードを書いてしまうことがあります。(jsを例にします)// 配列を初期化する let language = ['JavaScript', 'Python', 'Ruby', 'PHP', 'Java', 'Swift']; // C言語を追加 language.push('C'); // 配列でループ for (value of language) { // コンソールに出力 console.log(value); }全行にコメントをつけるなど絶対にやめましょう。
このコードの場合はコメントはまったくなしで良いです。コメントを書いたときの弊害
- 「配列を初期化する」など、ほとんどの人がコメントがなくても理解できるコードにコメントをつけるのは、わかる人からすると冗長に感じてイライラさせてしまうかもしれません。
- コメントだって実装の一部です。実装したら必ず保守がつきまといます。コードとコメントの両方を修正する2重作業がずっと発生します。
- コードだけ直してコメントを直し忘れる、なんてやっちゃったときにはあとから見た人が大混乱を起こします。
コメントなくても読みやすいコードを書くには
- 変数名やメソッド名の命名に最大限、気を使いましょう。
- ロジックが複雑になったらコメントをつけたい。ということがあると思いますが、設計から見直しましょう。
- 説明変数を作る、ネストを浅く保つ、メソッドに切り出すetc...コメントがなくても可読性の良いコードを書くテクニックを身に着けましょう。
コメントがあった方が良いパターン
- クラスやメソッドに対するDOCコメント(引数と返り値の説明のこと。ドキュメントに出力できる。)はプロジェクトとして書くかどうかルールを決めて、言語仕様に則って書きましょう。
- TODOコメント(あとでやること)はあってもOK。
- どうがんばっても複雑になってしまった場合はやむなくコメントを添えましょう。
- 投稿日:2021-01-23T17:21:17+09:00
node.jsのローカルサーバーでクライアントサイドjavascriptが動かない
はじめに
- 素人がタイトルの問題で詰まったので解決方法を残しておきます
- 解決方法の一つは@Suibari_chaさんの記事を大いに参考にさせてもらいましたが(ほぼそのままです)、「javascriptが動かない」でググると見つけづらかったので一応記載させていただきました
解決方法1
下記記事のSuibari_chaさんのcssを読み込む方法:Node.jsでhttpサーバを立てた際にCSSが読み取れない場合の対処法について
と同様に、javascriptのMIMEタイプを指定します。
サーバー起動時に「.jsはJavaScriptとして扱う」
というのを教えてあげないといけないということみたいです。
(node.jsなんだからそれくらいデフォルトで分かってくれよと思いますけども)解決方法2
expressを使います
まず、expressをインストール
npm install express下記のサーバー起動のスクリプトを書いて起動します。
index.js// node index.js で起動 var express = require('express'); var app = express(); app.use(express.static(__dirname)); // ここに入っているjsやcssを使う app.get('/', function (req, res) { //res.send('Hello World!'); res.sendFile(__dirname + '/index.html'); }); app.listen(3000, function () { console.log('Example app listening on port 3000!'); }); //http://localhost:3000にアクセスして確認こちらではいちいちMIMEタイプを指定しなくて良いみたいです。
ポイントはここです。app.use(express.static(__dirname)); // ここに入っているjsやcssを使う終わり
- 投稿日:2021-01-23T17:12:07+09:00
[フロントエンド] うわっ…Componentの凝集度、低すぎ?
0. はじめに
有名OSSのコミッターから、コピペで動かすマンまで、彼らは等しくプログラマと呼ばれます。10xプログラマという言葉があるように、同じプログラマでもその生産性には天地ほどの開きがあります。
プログラマの生産性は、1968年のSackmanらの研究以来、ソフトウェア工学でも熱い研究テーマの一つですが、未だにプログラマの生産性を測る指標は確立されていません。
一方、広木大地氏は自著「エンジニアリング組織論への招待」で、エンジニアリングを不確実性を削減する行為と定義しました。プログラミング能力を測る重要な尺度として、モジュールの凝集度があります。高い凝集度で設計しコーディングされたモジュールは、見通しがよく、再利用可能で、バグが少ない…つまり不確実性が少ない状態と言えるでしょう。
システムが指数関数的に複雑化し続ける昨今、凝集度という概念は、1つのクラスからインフラ構成に至るまで、ソフトウェア・エンジニアリングの全てのレイヤーで必要となる基本教養です。凝集度は言語に依存しない概念ですが、ここではWebのフロントエンドを題材に、凝集度を向上させる方法を実例と共に紹介します。
1. 対象読者
下記の人たちに宛てて書きます。
- 凝集度が何かよく知らない方
- 凝集度の高い設計にいまいち自信のない方
- クソコードを書いてスヤスヤ眠る毎日を過ごしていた過去の私
2. 類語
言葉の意味を知りたいとき、国語辞典より類語辞典を引いたほうが理解の助けになる場合があります。ここでは凝集度そのものの説明の前に、凝集度の類語をいくつか紹介します。
2-1. UNIX哲学 - Do one thing well
UNIXはソフトウェア界のバイキングたちが昼夜を問わず開発を進めている大規模なOSSです。そのUNIX開発を背骨のように支える思想を総称しUNIX哲学と呼びます。
UNIX哲学の中でも、様々なプロジェクトに適用できる思想が
Do one thing well=1つのことを上手くやれ
です。例えば、次のUNIXコマンドは
今いるディレクトリの.jsonファイルを個数を出力
します。$ ls bar.json foo.json fuga.gif hoge.txt piyo.json $ ls -l | grep '\.json$' | wc -l 3UNIXではプログラムを小さな粒度に保つべきという掟があります。複雑な処理を行いたいときは、上の例のように、それら小さなプログラムをパイプ
|
で連結して実現するのです。つまり個々のプログラムは、小さな責任を冴えたやり方で果たせ、すなわち
Do one thing well
であれという考えです。2-2. KISS原則
Keep it simple, stupid=シンプルにしておけ、バカ
、もしくはKeep it short and simple=簡潔かつシンプルしておけ
のアクロニムです。一般にコードは書かれる時間よりも読まれる時間の方が長いです。したがって、コーディングに時間を掛けてでも、モジュールはリーダブルである必要があります。
- リーダブルなコードについては「Rubyで学ぶ1年目に知っておきたいプログラミング技法8選」という記事を書きました。
また一番美しいコードは0行のコードだという極論が示すように、設計のシンプルさはシステムの堅牢性と明白な因果関係があります。スペースシャトルよりもソユーズの耐用年数が長いのも、AK-47が世界で最も使われる銃であるのも、これらの設計がシンプルであることと無関係ではありません。
3. 凝集度
上で述べた類語に共通することはなんでしょうか。それは
モジュールの責任を減らす
という考え方です。
Do one thing well
、KISS原則
を現実世界のコードに反映させる具体的な指標が凝集度です。「ソフトウェアの複合/構造化設計」によれば、凝集度はその巧拙のレベルにより次の7つに分かれます。
- 偶発的凝集(最悪)
- 論理的凝集
- 時間的凝集
- 手続き的凝集
- 通信的凝集
- 逐次的凝集
- 機能的凝集(最良)
しかしこの7つのレベルは細分化されすぎており境界が曖昧です。知っておくことは決して無駄ではありませんが、憶えていてもそれほど実務では役に立ちません。
また凝集度の低さを定量的に測るLCOM*という指標があり、次式で表されます。
LCOM* = \frac{\frac{1}{a}\sum_{j=1}^{a}\mu(A_j)-m}{1-m}
変数 説明 $a$ クラスのメンバ変数の個数 $A_j$ クラスのj番目のメンバ変数 $m$ クラスのメソッドの個数 $\mu(A_j)$ $A_j$にアクセスしているメソッドの個数 しかしLCOM*は古き良きオブジェクト指向言語のクラス設計には有効ですが、特に昨今のフロントエンドでデファクトスタンダードとされる設計手法から見ると、時代遅れの感は否めません。
したがってこの記事では、現実世界のプロジェクトで如何に凝集度が低下するか、具体例を使って示します。
4. 凝集度が下がる瞬間
フロントエンド開発においてComponentの凝集度を上げると言った場合、Componentの責任を可能な限り小さく保つことを指します。
例えばユーザーのプロフィールを表示する次のようなProfile Componentを考えます。
const Profile = ({user: UserModel}) => { return ( <> <img src={user.avatar} /> <h2>Hi, I'm {user.name}.</h2> <> ) }
user
という単一の引数を持ったシンプルなComponentで、プロフィールを表示する
という責任に集中しています。よって凝集度は最良と言えます。しかしここで「プロフィールと一緒に友達のリストを表示する」という要件が追加されたとします。Profile Componentは次のようになりました。
const Profile = ({user: UserModel}) => { // 友達をロードして const friends: FriendModel[] = loadFriends(user.id) // 友達リストのNodeを作る const friendsNode = ( <ul> {friends.map(friend => <li key={friend.id}>{friend.name}</li>)} </ul> ) return ( <> <img src={user.avatar} /> <h2>Hi, I'm {user.name}.</h2> {friendsNode} <> ) }しかしあなたはProfile Componentが既に複数の場所から呼ばれており、友達を表示したくないケースがあることに気が付きます。そのため友達リストの表示をコントロールできるよう、Profile Componentに分岐処理を加えました。
const Profile = ({user: UserModel, shouldShowFriends: bool}) => { // 友達リストのNodeを生成する関数 const showFriends = () => { // 非表示ならreturn null if (!shouldShowFriends) return null // 表示するなら友達をロードしてNodeを生成 const friends: FriendModel[] = loadFriends(user.id) return ( <ul> {friends.map(friend => <li key={friend.id}>{friend.name}</li>)} </ul> ) } return ( <> <img src={user.avatar} /> <h2>Hi, I'm {user.name}.</h2> {showFriends()} <> ) }引数に依存した分岐処理が加わりました。凝集度が低下した瞬間です。
Profile Componentは、
プロフィールを表示する
という唯一の責任を果たす美しいモジュールでした。それがいまや分岐処理を加えられ、友達リストを表示する
という新たな責任を負ってしまいました。5. 凝集度を高く保つ
それでは「友達リストを表示する」という要件に対して、どのようなComponentを書けば良かったのでしょうか。
当然答えは
友達リストを表示する
という単一の責任だけを負った新たなComponentを作る、です。Profile Componentからコードを分離し、FriendsというCompnentを作ります。const Friends = ({user: UserModel}) => { const friends: FriendModel[] = loadFriends(user.id) return ( <ul> {friends.map(friend => <li key={friend.id}>{friend.name}</li>)} </ul> ) }そしてあとは、Profile Componentを呼んでいた親Componentに、Friends Componentを追加してあげれば良いのです。
// 親Componentから個々のComponentをコール <Profile user={user} /> <Friends user={user} />これで個々のComponentの凝集度を高く保ったまま、要件に応えることができました。
またFriends Componentを分離したことで、「友達リストを表示する」という機能に対して、以後は同じFriends Componentを再利用すれば良くなった点も重要です。
Componentにコードを書き足す前に、その修正によりComponentの責任を増やすことにならないか、自分に問いかけてみましょう。もし答えがYESなら、それはComponentを分割するタイミングであるはずです。
6. 結合度
凝集度とセットで語られる概念として結合度があります。これらは混同されやすい概念ですが、図示すると分かりやすいです。図の中の円は、引数、メンバ変数、メンバ関数といった要素を示します。
凝集度の対象はモジュール単体です。モジュールの担う責任の少なさ、要素の少なさ、要素同士の関連度によって決定します。高いほど善です。
一方、結合度の対象はモジュール同士の関連です。モジュール同士の結びつきの強さを表します。低いほど善です。
ただし凝集度と結合度は反比例の関係にあります。高い凝集度を意識し書かれたモジュールは、自ずと低い結合度になります。結合度への意識が必要なケースもありますが、フロントエンド開発においては、凝集度さえ意識できていれば問題ないというのが今の所の結論です。
7. おわりに
ソフトウェア・エンジニアリングの世界は流行り廃りが激しい一方、どのようなプロジェクトでも有用な教養があります。そのうちの一つが凝集度という古くから存在する尺度です。
特に昨今のシステムは指数関数的に複雑化しており、モジュールの凝集度を高く保つことはほぼ全ての職業エンジニアに求められます。
Microserviceアーキテクチャや関数型言語といったトレンドの底流にあるのも、凝集度と同じく、小さな責任を冴えたやり方で果たすという考えです。
- 投稿日:2021-01-23T17:04:21+09:00
手近なものを使って衝突音サンプルを作ってみた/手順メモ
物理エンジンで物体を自由落下させて、衝突時に音を鳴らすようにしてみた
で音を鳴らしてみたのですが、ただのサイン波だとイマイチなので、ビンとか食器の衝突音を録音してみました。
(フリーのサンプルとか探せばでてくるかもですが。。)無料で使ってるCodePenにあまり巨大なデータを置くのは忍びないので1、サンプルはごく少量です。
(単純に録音に飽きた説)See the Pen Sound Sample (ビンや食器の衝突音) by kob58im (@kob58im) on CodePen.
手順メモ
0. 録音機材を準備する
パソコンがマイク内蔵している場合もあります。(自分の持っているパソコンは内蔵してました。)
ここの選定が命な気がしますが、まず試してみる分には、ありものでいいんじゃないかなと思います。【注意点】
自分は音響には詳しくないので、細かいことはわかりませんが、最近はパソコンにマイク端子やスピーカー端子の挿し口がない場合も多いので、あらかじめ確認しましょう。
あと、端子のタイプも色々あるようなので、注意が必要。0'. 録音環境を準備する
極力無音の環境を準備する。(エアコンや空気清浄機を録音中は止めておくとか。)
1. 録音する
参考サイト1. Windows 10 の「ボイスレコーダー」を使い、音声や音楽の録音を行う方法 に詳しく書かれているので、本記事では省略します。
2. 加工する
参考サイト2. 「Sazanami」老舗の無料サウンドエディター - 窓の杜 などの、音声波形加工ツールを使って、加工します。
加工する前に、音がしっくりくるか?を波形を部分再生して確認します。
波形がサチってないか(加減上限に張り付いてつぶれていないか)、小さすぎないか、についても見ておくとよいです。マイク音量を調整したり、音を鳴らす場所(マイクとの距離)を調整したりすると改善するかもしれません。とりあえず、自分がやった点は下記です。
- 欲しい波形部分を切り出す。
- 音量を正規化する。
- 波形の立ち上がりまでの時間を極力短くする。(波形を時間軸方向に拡大して、ほかの波形との開始タイミングを一定に保つのがよい。2)
- 波形の終わりを適当に決めて、切り詰める。(ここはテキトウにやってます。)
- 開始部分を選択してフェードイン加工する。
- 終了部分を選択してフェードアウト加工する。
フォーマットの変換
ファイルサイズを小さく抑えるためでもあるが、波形間で統一するのがよい。
※基本的にはフォーマット変換すると音質が劣化するので、聴くに堪えれる音質かどうかをチェックしながら決めるのがよいかと思います。
今回のサンプルは、モノラル(1ch)、8bit/サンプル、サンプリングレート22050Hzにしました。
- サンプリングレートを一定の値にする。
- 1ch(モノラル)にする。
- ビットを一定の値にする。3
3. 不要なヘッダ情報の削除
個人情報の流出防止とか、ファイルサイズの観点から、不要なヘッダ情報(録音者とかコメントとか)は削除したほうがよいです。
加工の際に参考サイト3で変換した結果、ヘッダ情報が消えたようなので、今回はそれで兼ねている。。。(参考サイト3が保証しているわけではないので自己責任でお願いします。。。)
そのうちwavヘッダ情報について調べたいと思います。
- wav ヘッダ フォーマット - Google 検索参考サイト
- Windows 10 の「ボイスレコーダー」を使い、音声や音楽の録音を行う方法 - k本的に無料ソフト・フリーソフト
- 「Sazanami」老舗の無料サウンドエディター - 窓の杜
- Waveファイルのフォーマット変換
- ドラッグ&ドロップされた画像や音声ファイルをインラインimgやaudioタグに変換するツールをつくってみた - Qiita
容量についての記載ありました:Pen Limitations - CodePen Blog ↩
今回の波形データでは、時間を測らずテキトウにやってます。。。 ↩
ここだけ参考サイト2のツールでのやりかたが分からず、参考サイト3で実施しました。 ↩
- 投稿日:2021-01-23T17:04:21+09:00
手近なものを使って衝突音サンプルを作ってみた
物理エンジンで物体を自由落下させて、衝突時に音を鳴らすようにしてみた
で音を鳴らしてみたのですが、ただのサイン波だとイマイチなので、ビンとか食器の衝突音を録音してみました。
(フリーのサンプルとか探せばでてくるかもですが。。)無料で使ってるCodePenにあまり巨大なデータを置くのは忍びないので1、サンプルはごく少量です。
(単純に録音に飽きた説)See the Pen Sound Sample (ビンや食器の衝突音) by kob58im (@kob58im) on CodePen.
参考サイト
- Windows 10 の「ボイスレコーダー」を使い、音声や音楽の録音を行う方法 - k本的に無料ソフト・フリーソフト
- 「Sazanami」老舗の無料サウンドエディター - 窓の杜
- Waveファイルのフォーマット変換
- ドラッグ&ドロップされた画像や音声ファイルをインラインimgやaudioタグに変換するツールをつくってみた - Qiita
容量についての記載ありました:Pen Limitations - CodePen Blog ↩
- 投稿日:2021-01-23T16:00:44+09:00
CSS/JSでおしゃれなタブを実装!
こちらを参考に、
おしゃれなタブ切り替えをポートフォリオサイトに実装しました。タブをクリックすると、タブの下線が移動して切り替わる、非常にシンプルなタブです。
各タブの内容がディレイ表示されます。HTMLを記述
HTML<h1>Lava lamp multiple tabs</h1> <div class='tabs'> <div class='tab-buttons'> <span class='content1'>Button 1</span> <span class='content2'>Button 2</span> <span class='content3'>Button 3</span> <div id='lamp' class='content1'></div> </div> <div class='tab-content'> <div class='content1'> This is the content of 1 container.This will be open when button 1 is clicked.This is the content of 1 container.This will be open when button 1 is clicked.This is the content of 1 container.This will be open when button 1 is clicked. </div> <div class='content2'> This is the content of 2 container.This will be open when button 2 is clicked.This is the content of 2 container.This will be open when button 2 is clicked.This is the content of 2 container.This will be open when button 2 is clicked. </div> <div class='content3'> This is the content of 3 container.This will be open when button 3 is clicked.This is the content of 3 container.This will be open when button 3 is clicked.This is the content of 3 container.This will be open when button 3 is clicked. </div> </div> </div> <div class='credit'> </div>
tab-buttons
でタブに表示させる名前、
tab-content
でタブ内に表示させるコンテンツを表示しています。CSSを記述
CSS@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300); body{ padding:0; margin:0; background:#fff; } h1{ text-align:center; font:300 40px 'open sans',sans-serif; color:#666; text-transform:uppercase; } .tabs{ width:50%; margin:10px auto; position:relative; } .tab-buttons span{ font:400 14px 'open sans',sans-serif; color:#333; background:#eee; cursor:pointer; border-bottom:2px solid #ddd; display:block; width:33.3%; float:left; text-align:center; height:40px; line-height:40px; } .tab-content{ border-bottom:3px solid #ddd; padding:15px; background:#eee; display:inline-block; font:400 13px 'open sans',sans-serif; color:#333; } #lamp{ width:33.3%; height:2px; background:#333; display:block; position:absolute; top:40px; transition: all .3s ease-in; -o-transition: all .3s ease-in; -webkit-transition: all .3s ease-in; -moz-transition: all .3s ease-in; } #lamp.content2{ left:33.3%; transition: all .3s ease-in; -o-transition: all .3s ease-in; -webkit-transition: all .3s ease-in; -moz-transition: all .3s ease-in; } #lamp.content3{ left:66.6%; transition: all .3s ease-in; -o-transition: all .3s ease-in; -webkit-transition: all .3s ease-in; -moz-transition: all .3s ease-in; } #lamp.content1{ left:0; transition: all .3s ease-in; -o-transition: all .3s ease-in; -webkit-transition: all .3s ease-in; -moz-transition: all .3s ease-in; }自身のサイトに実装するときは、
body
あたりの記述はいったん削除しました。JavaScriptを記述
JavaScript$('.tab-content>div').hide(); $('.tab-content>div').first().slideDown(); $('.tab-buttons span').click(function(){ var thisclass=$(this).attr('class'); $('#lamp').removeClass().addClass('#lamp').addClass(thisclass); $('.tab-content>div').each(function(){ if($(this).hasClass(thisclass)){ $(this).fadeIn(800); } else{ $(this).hide(); } }); });まとめ
Javascriptの記述が簡単なので、とても簡単に実装できました!
jQueryの読み込みをしていないと動かないのでそこだけ注意です◎
- 投稿日:2021-01-23T14:20:02+09:00
3Dあみだくじ(物理)
例によって、やってみたかっただけ
See the Pen 3D ladder lottery by using cannon.js by kob58im (@kob58im) on CodePen.
- 投稿日:2021-01-23T12:39:21+09:00
[HTML5] datetime-localで,placeholderが表示されない問題を解決する
datetime-localとは
日付と時刻を1つのウィンドウで入力できる,input要素の型です。
詳しくは,MDN Web Docsで説明されておりますので,下記URLからご確認ください。
https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/datetime-localブラウザーの対応状況
2021年1月23日時点の,"Can I Use..."の情報 ( https://caniuse.com/?search=datetime-local ) によると,
- モバイルブラウザー: ほとんどが対応しています。(Opera MiniとKaiOS Browserを除く)
- デスクトップブラウザー: IE,Firefox,Safariが非対応となっています。 (ただし,SafariのTechnology Preview版では対応とのこと→次期バージョンのSafariは,対応するかも?)
モバイルではほぼ導入が完了している一方で,デスクトップでは導入に消極的です。
結論: アクセスをモバイル端末に限定させるか,Cordovaを利用してアプリにするならば,問題はなさそうです。
本題: Placeholderが表示されない!
なぜかiOS Safari等で,placeholderが表示されません。
そこで,:beforeとカスタム属性を使って,強引に解決させます。コード
HTML
<input type="datetime-local" id="target">CSS
#target { position: relative; width: 50%; height: 10vh; font-size: 14px; -webkit-appearance: none; -moz-appearance: none; appearance: none; border: 1px solid #333; border-radius: 0; outline: none; } #target::before { position: absolute; width: 100%; height: 100%; padding-left: 10px; font-size: 14px; line-height: 10vh; top: 0;bottom: 0;left: 0; right: 0; content: attr(placeholder2); } #target:focus{ background: #333; color: #fff; }JS
const placeholder="日時を選択",target=document.getElementById('target'); target.setAttribute('placeholder2',placeholder); const focusplaceholder=()=>{ event.target.setAttribute('placeholder2', ' '); } const inputplaceholder=()=>{ if(event.target.value){ event.target.setAttribute('placeholder2', ' '); }else{event.target.setAttribute('placeholder2',placeholder);} } target.addEventListener("focus",focusplaceholder); target.addEventListener("input",inputplaceholder);注意事項
上記コードは,モバイルブラウザーでの利用(or Cordovaでの開発)が前提です。
デスクトップブラウザーの場合,ブラウザーデフォルトの"yyyy/mm/dd --:--"と,上記で設定したplaceholderは重なります。それゆえ,使い物にはならないでしょう。コードは,各自で改良していただければと思います。もっと綺麗に書ける方法があれば,ご教示いただければ幸いです。
- 投稿日:2021-01-23T12:24:04+09:00
Next.js 開発環境構築テンプレ(TypeScript、ESLint、Prettier)
はじめに
Next.jsのプロジェクト作成のテンプレートです。自分用ですが、毎回調べるのは面倒なのでのでメモしておきます。
1.Next.jsのプロジェクトの作成
npx create-next-app $PROJECT_NAME
2.ベースディレクトリを変更
pages と styles を src のディレクトリ内に移動。
mkdir src && \ mv pages/ src/pages & \ mv styles/ src/styles3.TypeScriptを導入
npm install -D typescript @types/react @types/react-dom @types/node4. ESLintとPrettierを導入
エラーを事前に取り除いたり、コードを整形するため
npm install -D eslint prettier eslint-config-prettierESLintで使うプラグインもインストール
npm install -D eslint-plugin-{react,react-hooks,prettier} npm install -D @typescript-eslint/{parser,eslint-plugin}5.ESLintルール設定
.eslintrc.json{ "env": { "es6": true, "node": true, "browser": true, "commonjs": true }, "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly", "React": "writable" }, "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2018, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "settings": { "react": { "version": "detect" } }, "plugins": ["react-hooks", "react", "@typescript-eslint", "prettier"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:react-hooks/recommended", "prettier", "prettier/react", "prettier/@typescript-eslint" ], "rules": { "react/prop-types": "off", "prettier/prettier": "error", "react/react-in-jsx-scope": "off" }, "overrides": [ { "files": ["*.js"], "rules": { "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/explicit-function-return-type": "off" } } ] }6.Prettier設定
.prettierrc.json{ "semi": true, "trailingComma": "all", "singleQuote": true, "printWidth": 100, "tabWidth": 2 }7.VSCodeの自動整形
.vscode/settings.json{ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } }8.状態管理ライブラリを追加(オプション)
2020/05月にFacebookから出た新しい状態管理ライブラリです。
- 投稿日:2021-01-23T12:08:37+09:00
Nuxt.jsから #Obniz を操作するメモ(ゆるくTypeScriptも使うよ)
はじめに
Nuxt.jsからObnizを操作したくなったので、試してみました。
その手順をメモします。
お手元にObnizボードをご用意ください。create-nuxt-appを使って、Nuxt.jsアプリを作成する
公式ドキュメントのGET STARTEDを参考に、アプリを作成していきます。
以下のコマンドを実行します。
npx create-nuxt-app <project-name>
- 投稿日:2021-01-23T12:01:38+09:00
React-Reduxの使い方 (ReactとReduxの接続)
個人的なメモも兼ねて。
src/index.js
でやること1.
createStore
のメソッドをRedux
からimportする。
STORE
を作成するためのメソッド。
引数にreducer
を指定する(後述)。src/index.jsimport { createStore } from 'redux';2.
Provider
コンポーネントをimportReduxをReactで使用するためのMWである
react-redux
から、Reactプロジェクトで使用することのできるProvider
コンポーネントをimportし、設置する。
Provider
コンポーネントって何?React Reduxの公式ドキュメントにもあるように、
Provider
コンポーネントにネストされている範囲だけが、react-redux
の機能の一つであるconnect()
でconnectされたコンポーネントを扱えるようになるらしい。Normally, you can’t use a connected component unless it is nested inside of a
<Provider>
.
Provider
コンポーネントはstore
と言うpropsのみ受け付けている。文字通りこのpropsにはcreateStore()
で作成したSTORE
を渡す。src/index.jsimport { Provider } from 'react-redux'; ~~~~~中略~~~~~ ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') );3.
STORE
に渡すためのREDUCER
をimport
REDUCER
は他のファイルで定義しておく。
よく見かけるのはsrc/reducers
ディレクトリを切ってその中に目的別のreducerファイルを作成していく手法。複数のファイルにまたがる
REDUCER
を一つにまとめてsrc/index.js
でSTORE
に渡す方法は以下を参照。
Reactアプリ内でのReduxの複数のReducerを一つにまとめる方法 - QiitaREDUCERをまとめてimportimport rootReducers from "./reducers"; // 指定がない場合はwebpackがデフォルトでindex.jsというファイルを指定してくれるReduxを使用するときにやること
ReactとReduxの接続
connect()
登場人物たち
- mapStateToProps :STORE
が持っているstateをpropsに入れて子コンポーネントへ渡す。
- mapDispatchToProps :DISPATCH
を呼び出す関数をpropsに入れて子コンポーネントへ渡す。mapStateToPropsについてconst mapStateToProps = (state) => { // Issueコンポーネント内で使用するstateを限定する役目 return { // stateオブジェクトから指定できるのは、src/reducers/index.js でrootReducerにまとめたときのオブジェクトのkey名 // key名がコンポーネントで受け取るpropsの名前になる // なので、子コンポーネントでは「issue」という名前で参照可能。 issue: state.issue.data, }; }mapDispatchToPropsについて/* 子コンポーネントからdispatchするときに、わざわざ `dispatch(actionCreator(追加したい要素))`しなくても、 この場合だと `addContents(追加したい要素)` でstoreにdispatchできるようになる。 */ const mapDispatchToProps = dispatch => { return { addIssue: (payload) => { dispatch(actions.addIssue(payload)); }, } }
connect()
の書き方connect()の書き方const ContainerInMiddle = connect( mapStateToProps, mapDispatchToProps )(ChildComponent);
Container Components
(reduxと接続したreactのコンポーネント)のコード例import * as actions from '../actions'; import { connect } from 'react-redux'; import IssueContents from '../components/organisms/IssueContents'; const mapStateToProps = (state) => { // Issueコンポーネント内で使用するstateを限定する役目 return { // stateオブジェクトから指定できるのは、src/reducers/index.js でrootReducerにまとめたときのオブジェクトのkey名 // key名がコンポーネントで受け取るpropsの名前になる // なので、子コンポーネントでは「issue」という名前で参照可能。 issue: state.issue.data, }; } /* 子コンポーネントからdispatchするときに、わざわざ `dispatch(actionCreator(追加したい要素))`しなくても、 この場合だと `addContents(追加したい要素)` でstoreにdispatchできるようになる。 */ const mapDispatchToProps = dispatch => { return { addIssue: (payload) => { dispatch(actions.addIssue(payload)); }, } } export default connect( mapStateToProps, mapDispatchToProps )(IssueContents);参考
▷Reduxのconnectについて
▷Basic Tutorial | React Redux
▷ReactとReduxで管理するstateの分け方|Playground発!アプリ開発会社の技術ブログ
- 投稿日:2021-01-23T09:53:00+09:00
変数の巻き上げ
内容
変数の巻き上げについて書く。
変数の巻き上げ
グローバルスコープ内で変数を宣言すると、宣言処理はグローバルスコープの先頭に巻き上げられる。
例えば、以下のようなコードがあったとする。
console.log("1行目:" + x); // 変数の宣言前に変数xを使用 var x = "initialize"; // 変数xを宣言 console.log("3行目:" + x);普通に考えると、1行目は変数の宣言前なのでReferenceErrorになる気がする。
が、実行してみるとこうなる。1行目:undefinedこれは、変数の巻き上げによって、変数xの宣言処理がコードの先頭に巻き上げられたため。
イメージとしては、こんな感じになっている。var x; // 変数の宣言処理だけ、先頭に移動 console.log("1行目:" + x); x = "initialize"; // 変数の初期化処理はそのままの位置 console.log("3行目:" + x);関数内での変数の巻き上げ
関数内で変数が宣言されている場合も変数の巻き上げは発生する。
関数内で変数が宣言されている場合は、その関数の先頭に巻き上げられる。例えば、こんなコードがあったとする。
これは関数内での巻き上げが発生しないやつ。var x = "A"; function testPrint(){ console.log(x); } testPrint();こいつの動作結果は、こんな感じ。イメージ通りの動きである。
A一方、これは関数内で変数を宣言して巻き上げが発生するやつ。
var x = "A"; function testPrint(){ console.log(x); var x = "B"; } testPrint(); // undefinedこの実行結果は、こうなる。
undefined何でそうなるかっていうと、巻き上げによってこんな感じの動作になっているから。
関数の先頭でxが再宣言されることで、console.log(x)がundefined(未定義)になるんすね。var x; x = "A"; function testPrint(){ var x; console.log(x); x = "B"; } testPrint(); // undefined関数の宣言も巻き上がるぞ
変数だけじゃなくて関数も巻き上がる。
関数の巻き上げは、割と自然に受け入れられる。
例えば、これは巻き上げの例。testPrint("Yamada"); function testPrint(item) { console.log(item); }こいつの実行結果は、こうなる
Yamada割と普通の動きに見える。
でも、よく考えるとtestPrint関数が定義される前にtestPrint関数を呼び出しているのに、普通に呼び出せてるのはなんでだろう。
理由は、巻き上げによってこんな感じの動作になってるから。function testPrint(item) { // 巻き上げで関数の定義処理が先頭に移動した console.log(item); } testPrint("Yamada");つまり、巻き上げがあることによって、関数はどのタイミングでも呼び出せるようになってるってこと。
でも、関数式は巻き上がらないぞ
ややこしいけど関数式を使うとまきあがらない
関数式は宣言じゃないからね。関数式を代入された変数の方は、もちろん巻き上がる。これは、変数が巻き上がることを確認するコード。
console.log(myFunction); // => undefined var myFunction = function() { console.log('Hello'); };こっちは、関数式でかかれた関数は巻き上がらないことを確認するコード。
myFunction(); // => TypeError: myFunction は関数ではない var myFunction= function() { console.log('Hello'); };変数であっても定義を書かなければ巻き上がらない
変数であっても、定義処理を書かなかったら巻き上がらない。
例えば、これは巻き上がらないconsole.log(x); // => ReferenceError 例外が発生 x= “initialize”; // varを省略して初期化letやconstは、巻き上がらない(ようにふるまう)ので安心
letやconsotを使うと巻き上がりを無視して実装できる。
正確にはletもconsotも巻き上がるが、変数の初期化処理が行われるまでJavaScriptが一時的にプログラムから見えなくしてくれる。ので、巻き上がってないかのように使える。こんなコードがあるとする。
console.log(x); // => ReferenceErrorが発生。巻き上がってないように見える。 let x = “initialize”; // 変数xの宣言処理実際は、こんな感じで巻き上がるけ。
けど、不思議な大地の力で他の処理から変数が見えなくなってなかったことにされる。let x; // 宣言処理は先頭に巻き上がる console.log(x); // => ReferenceErrorが発生。不思議な力で変数xが隠される。 x = “initialize”; // 初期化参考
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Grammar_and_types
https://developer.mozilla.org/ja/docs/Glossary/Hoisting
- 投稿日:2021-01-23T09:53:00+09:00
変数の巻き上げをたしなむ
内容
変数の巻き上げについて書く。
変数の巻き上げ
グローバルスコープ内で変数を宣言すると、宣言処理はグローバルスコープの先頭に巻き上げられる。
例えば、以下のようなコードがあったとする。
console.log("1行目:" + x); // 変数の宣言前に変数xを使用 var x = "initialize"; // 変数xを宣言 console.log("3行目:" + x);普通に考えると、1行目は変数の宣言前なのでReferenceErrorになる気がする。
が、実行してみるとこうなる。1行目:undefinedこれは、変数の巻き上げによって、変数xの宣言処理がコードの先頭に巻き上げられたため。
イメージとしては、こんな感じになっている。var x; // 変数の宣言処理だけ、先頭に移動 console.log("1行目:" + x); x = "initialize"; // 変数の初期化処理はそのままの位置 console.log("3行目:" + x);関数内での変数の巻き上げ
関数内で変数が宣言されている場合も変数の巻き上げは発生する。
関数内で変数が宣言されている場合は、その関数の先頭に巻き上げられる。例えば、こんなコードがあったとする。
これは関数内での巻き上げが発生しないやつ。var x = "A"; function testPrint(){ console.log(x); } testPrint();こいつの動作結果は、こんな感じ。イメージ通りの動きである。
A一方、これは関数内で変数を宣言して巻き上げが発生するやつ。
var x = "A"; function testPrint(){ console.log(x); var x = "B"; } testPrint(); // undefinedこの実行結果は、こうなる。
undefined何でそうなるかっていうと、巻き上げによってこんな感じの動作になっているから。
関数の先頭でxが再宣言されることで、console.log(x)がundefined(未定義)になるんすね。var x; x = "A"; function testPrint(){ var x; console.log(x); x = "B"; } testPrint(); // undefined関数の宣言も巻き上がるぞ
変数だけじゃなくて関数も巻き上がる。
関数の巻き上げは、割と自然に受け入れられる。
例えば、これは巻き上げの例。testPrint("Yamada"); function testPrint(item) { console.log(item); }こいつの実行結果は、こうなる
Yamada割と普通の動きに見える。
でも、よく考えるとtestPrint関数が定義される前にtestPrint関数を呼び出しているのに、普通に呼び出せてるのはなんでだろう。
理由は、巻き上げによってこんな感じの動作になってるから。function testPrint(item) { // 巻き上げで関数の定義処理が先頭に移動した console.log(item); } testPrint("Yamada");つまり、巻き上げがあることによって、関数はどのタイミングでも呼び出せるようになってるってこと。
でも、関数式は巻き上がらないぞ
ややこしいけど関数式を使うとまきあがらない
関数式は宣言じゃないからね。関数式を代入された変数の方は、もちろん巻き上がる。これは、変数が巻き上がることを確認するコード。
console.log(myFunction); // => undefined var myFunction = function() { console.log('Hello'); };こっちは、関数式でかかれた関数は巻き上がらないことを確認するコード。
myFunction(); // => TypeError: myFunction は関数ではない var myFunction= function() { console.log('Hello'); };変数であっても定義を書かなければ巻き上がらない
変数であっても、定義処理を書かなかったら巻き上がらない。
例えば、これは巻き上がらないconsole.log(x); // => ReferenceError 例外が発生 x= “initialize”; // varを省略して初期化letやconstは、巻き上がらない(ようにふるまう)ので安心
letやconsotを使うと巻き上がりを無視して実装できる。
正確にはletもconsotも巻き上がるが、変数の初期化処理が行われるまでJavaScriptが一時的にプログラムから見えなくしてくれる。ので、巻き上がってないかのように使える。こんなコードがあるとする。
console.log(x); // => ReferenceErrorが発生。巻き上がってないように見える。 let x = “initialize”; // 変数xの宣言処理実際は、こんな感じで巻き上がるけ。
けど、不思議な大地の力で他の処理から変数が見えなくなってなかったことにされる。let x; // 宣言処理は先頭に巻き上がる console.log(x); // => ReferenceErrorが発生。不思議な力で変数xが隠される。 x = “initialize”; // 初期化参考
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Grammar_and_types
https://developer.mozilla.org/ja/docs/Glossary/Hoisting
- 投稿日:2021-01-23T07:13:09+09:00
`slice(1, -1)`って何してるの - 回文の真偽判定を再帰的に解く
isPalindrome(): A recursive approach
What Doesslice(1, -1)
Do?, (slice(1, -1)
って何してるの)
を元ネタにした日本語によるまとめ。要約
メジャーなプログラミング題材である、「回文の真偽判定」。
Project Euler in JavaScript
js 回文の真偽判定教科書どおり
function isPalindrome (str) { let len = 0; // remove non-alphanumeric characters and // change the string to lowercase // and get the length of the string str = str.replace(/[^a-z0-9]/i, '').toLowerCase(); len = str.length; // calculate the string midpoint position and // loop through the characters up to the midpoint // comparing characters in corresponding positions // from the start of the string and the end of the string for (let i = 0, mid = len >> 1; i < mid; i++) { if (str[i] !== str[len - i - 1]) return false; } // if execution reaches here, the character comparisons matched // and the string (if not empty) must be a palindrome return len > 0; }sliceでこう書ける
function isPalindrome (str) { // remove non-alphanumeric characters and // change the string to lowercase str = str.replace(/[^a-z0-9]/i, '').toLowerCase(); // and get the length of the string const len = str.length; if (len <= 1) return true; if (str[0] !== str[len - 1]) return false; // proper tail call optimized recursion return isPalindrome(str.slice(1, -1)); }内部再帰関数
_isPalindrome()
const isPalindrome = (() => { /** * This function is returned immediately * from the invocation of the outer arrow function * and is assigned to the `isPalindrome` identifier. */ return function isPalindrome (str) { // remove non-alphanumeric characters and // change the string to lowercase str = str.replace(/[^a-z0-9]/i, '').toLowerCase(); // call the recursive _isPalindrome function with string (if not empty) // and return the result return (str.length > 0) && _isPalindrome(str); }; /** * Internal recursive `_isPalindrome()` function * optimized for recursion with proper tail call. * * A single reference to this function is created and stored * after the immediate invocation of the outer arrow function, * not accessible outside the scope of the outer arrow function, * but accessible to `isPalindrome()` via closure. */ function _isPalindrome (str) { const len = str.length; if (len <= 1) return true; if (str[0] !== str[len - 1]) return false; // proper tail call return _isPalindrome(str.slice(1, -1)); } })();更に最適化
const isPalindrome = (() => { return function isPalindrome (str) { str = str.replace(/[^a-z0-9]/i, '').toLowerCase(); // wrap the recursive _isPalindrome function with _trampoline() return (str.length > 0) && _trampoline(_isPalindrome)(str); }; // trampoline() — higher-order function function _trampoline (fn) { return function _trampolined (...args) { let result = fn(...args); while (typeof result === 'function') { result = result(); } return result; } } function _isPalindrome (str) { const len = str.length; if (len <= 1) return true; if (str[0] !== str[len - 1]) return false; // return a function that calls the recursive function return () => _isPalindrome(str.slice(1, -1)); } })();sliceとは
javascript sliceメソッド
javascriptのStringのsubstring slice substrconst str = "deluxe"; console.log(str.slice(2)); // luxeが返る console.log(str.slice(2, 4)); // luが返る console.log(str.slice(1, 6)); // eluxeが返る console.log(str.slice(1, -1)); // eluxが返るconst str = 'マツコ'; console.log(str.slice(1, 2)); console.log(str.slice(1, -1)); //いずれも ツが返るconst str = 'マツコDeluxe'; console.log(str.slice(1, 2)); // ツが返る console.log(str.slice(1, -1)); // ツコDeluxが返るつまり What Does
slice(1, -1)
Do?, (slice(1, -1)
って何してるの)、今日は、slice(1, -1)が何をするのかを学んだ。
文字列が回文かどうかを (再帰的に) チェックする方法を調べていたら、 str.slice(1, -1) を使う解決策に出くわしました。配列を変異させずに処理したい場合は slice() が良い選択肢となります。
負のインデックスを使用して、シーケンスの終わりからを示すことができます。という話でありました。
- 投稿日:2021-01-23T02:28:57+09:00
[PlayCanvas] Assetの動的読み込み
はじめに
PlayCanvasのアセットはPreloadにチェックが入ってるものは全て起動時に読み込みます。それでは重いですし、何より全てのアセットが起動時に必要とも限らないので、必要最低限のアセットだけにPreloadにチェックを入れ、他のアセットは必要に応じて動的に読み込む必要があります。
方法
読み込むだけ
アセットの読み込みは
app.assets.load()
を使用するだけです。Samplevar Sample = pc.createScript('sample'); Sample.attributes.add('picture', {type: 'asset'}); Sample.prototype.initialize = function() { this.app.assets.load( this.picture ); };
load()
はまだ読み込まれていないデータをロードするメソッドです。すでに読み込まれている場合は何もしません。読み込みを待ってから処理を実行する
先の方法は読み込むだけなので、読み込まれたら反映をする必要があります。それを実現する為に
ready()
を使用します。Samplevar Sample = pc.createScript('sample'); Sample.attributes.add('picture', {type: 'asset'}); Sample.prototype.initialize = function() { this.picture.ready( this.onLoaded ); this.app.assets.load( this.picture ); }; Sample.prototype.onLoaded = function( asset ) { this.entity.element.textureAsset = asset ; };
ready()
は読み込みが完了していた時に処理を行うメソッドです。
すでに読み込まれていれば即時実行されます。まとめて読み込む
上記の方法は、1つのアトリビュートに1つの処理を書いているので、数が増えれば当然煩雑になります。ある程度決まったルールがあるのであれば、アセットタグを使用した読み込みを実装するべきです。
Samplevar Sample = pc.createScript('sample'); Sample.prototype.initialize = function() { var assets = this.app.assets.findByTag( 'Tag' ); var self = this; assets.forEach(( elm )=>{ self.app.assets.load( elm ); }); };おわりに
覚書程度ですが、一先ず基本的な手段を書きました。
凝ったことをしなければ、これで十分なはずです。(多分)おまけ
ここからはただの個人的覚書
一連の処理をメソッド化する
紹介したコードを全ての場所に書いていたらはっきり言って手間ですし汚くなるのは目に見えてるので、一連の動作をメソッド化します。
function checkLoad( asset, callback ){ const app = pc.Application.getApplication(); if( asset ){ asset.ready( callback ); app.assets.load( asset ); } }プロジェクト外のリソースを読み込む
定期的にキャラを追加するゲームだったとして、キャラを追加する度にプロジェクトにインポートしてビルドしてサーバーに上げるのは非効率なので、そういったデータは別に管理するとなったとしましょう。そういったデータを読み込む方法として
loadFromUrl()
というメソッドが用意されています。var Sample = pc.createScript('sample'); Sample.prototype.initialize = function() { const url = ""; const type = "texture"; this.app.assets.loadFromUrl( url, type, this.onLoaded ); }; Sample.prototype.onLoaded = function( asset ) { this.entity.element.textureAsset = asset ; };
- 投稿日:2021-01-23T01:32:15+09:00
え、まだエラー処理を一括管理していないの?
まずは謝ります。
タイトルで煽ってしまい申し訳ありません。
ところで皆さん、まずは下記のエラーメッセージをご覧ください!
NavigationDuplicated: Avoided redundant navigation to current location
vue-routerを使っている方は、一度は遭遇しているであろうこのエラー。
意味は、「同じページに遷移できないよ!」と言う意味です。
まずは、このエラーの回避策をお教えします。
this.$router.push('/').catch(() => {})たった
catch(() => {})
を付けるだけでエラーが出なくなります!これでエラーを解決した気にならずにもうちょっと我慢して見て下さい。
では、大規模な開発になった場合、いちいち
catch(() => {})
を付けるとなると面倒ですよね???そこで、出てくるのがerrorHandlerというモジュールです!
これは、Vue特有のエラーを検知してくれる優れものです。
これは、同じようなエラーメッセージが何度も出てる場合に有効です。
使い方としては、エラーメッセージごとに処理を分けて使うといった感じです。
それでは使い方を見てみましょう!
main.js
にて以下のように関数の宣言を行います。main.jsVue.config.errorHandler = function (error) { //処理 }※注意 アロー関数にするとエラーが起きます!!!
次に、今回のエラーを例に処理を書いていきます。
main.jsVue.config.errorHandler = function (error) { if (error.name === 'NavigationDuplicated') { // routerで遷移する時、同じページに遷移しようとすると起こるエラーを回避 return } }このようにして、エラーメッセージを表示させることなく処理を終えることができます!
これはめちゃくちゃ使えるのでぜひ皆さんも使ってみてください!!!
以上、「え、まだエラー処理を一括管理していないの?」でした!
良かったら、LGTM、コメントお願いします。
また、何か間違っていることがあればご指摘頂けると幸いです。
他にも初心者さん向けに記事を投稿しているので、時間があれば他の記事も見て下さい!!
Thank you for reading
- 投稿日:2021-01-23T01:02:03+09:00
Vue.jsでページごとにhead,meta,titleを変える方法
皆さん、すべてのページでタイトルを同じにすると検索に引っかかりにくいってご存じですか?
他にも、サイトを作る際にパンくずリストを作っておくと、Googleロボットが認識しやすいだとか。
あまりSEOには詳しくないのですが、ここらへんの基礎知識には対応させた方が良いと考えているので、今回はタイトルの通り、ページごとに
head,meta,title
を変えていくパッケージをご紹介したいと思います。その名もvue-headです!
説明はこの辺にしておいて、解説していきます。
パッケージのインストール
まずは
vue-head
をインストールします。npm i vue-head --saveパッケージをインポート
次に、
main.js
にて以下のように記述します。main.jsimport Vue from 'vue' import VueHead from 'vue-head' Vue.use(VueHead)そしたら、後はコンポーネントにて好きなように設定していくだけ!
App.vue<script> export default{ head: { title: { inner: 'ザエモニア', separater: ' | ', complement: '会員登録' }, // グーグルリキャプチャのAPI取得 script: [ { type: 'text/javascript', src: 'https://www.google.com/recaptcha/api.js?render=' + process.env.VUE_APP_SITE_KEY, async: true } ] }, } </script>ただし、ここで注意してほしいのがscriptタグは
nuxt.js
やvue-router
でページ遷移する際にここで読み込まれたスクリプトファイルがそのまま残ってしまいます。画面自体に影響がなければ良いのですが、例えば「Google reCAPTCHA」では画面右下にロゴマークが出てしまうなど邪魔になる場合があります。
その時は、
window.location.href
などを設定することで、この問題を解消することができます。以下にその例を挙げます。
App.vue<script> export default{ beforeRouteLeave(to, from, next) { window.location.href = to.path } } </script>このようにすることで、スクリプトタグを次のページに持ち込まないようにしました。
ただ、このやり方はいかがなものかなと思います。
恐らくやり方としては適切な形ではないので、悪魔で参考程度に知っておいてください。
他のやり方を知っている方は、ぜひコメント欄にてご教示お願いします。
以上、「Vue.jsでページごとにhead,meta,titleを変える方法」でした!
良かったら、LGTM、コメントお願いします。
また、何か間違っていることがあればご指摘頂けると幸いです。
他にも初心者さん向けに記事を投稿しているので、時間があれば他の記事も見て下さい!!
Thank you for reading