- 投稿日:2020-05-28T23:33:19+09:00
はじめてのThree.js 5章 日記
- 投稿日:2020-05-28T22:43:38+09:00
JavaScript 配列と繰り返し
配列と繰り返し処理
配列と繰り返し処理の1例を紹介していきます。
(例) for文を使って、配列の要素を順番に取り出すsample.jsconst color = ["red", "blue", "yellow"]; for(let i = 0; i < color.length; i++) { //iが配列内の要素の数だけループする console.log(color[i];//変数iを用いて要素を取得 }red blue yellow
for文を使用することで配列の中に格納されているすべての値を簡単に出力することができます。
また、配列.length
とすることで配列の要素数を取得できます。
- 投稿日:2020-05-28T22:31:04+09:00
TypeScriptで学ぶデザインパターン〜Command編〜
対象読者
- デザインパターンを学習あるいは復習したい方
- TypeScriptが既に読めるあるいは気合いで読める方
- いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
- UMLが既に読めるあるいは気合いで読める方
環境
- OS: macOS Mojave
- Node.js: v12.7.0
- npm: 6.14.3
- TypeScript: Version 3.8.3
本シリーズ記事一覧(随時更新)
- TypeScriptで学ぶデザインパターン〜Iterator編〜
- TypeScriptで学ぶデザインパターン〜Adapter編〜
- TypeScriptで学ぶデザインパターン〜Template Method編〜
- TypeScriptで学ぶデザインパターン〜Factory Method編〜
- TypeScriptで学ぶデザインパターン〜Singleton編〜
- TypeScriptで学ぶデザインパターン〜Prototype編〜
- TypeScriptで学ぶデザインパターン〜Builder編〜
- TypeScriptで学ぶデザインパターン〜Abstract Factory編〜
- TypeScriptで学ぶデザインパターン〜Bridge編〜
- TypeScriptで学ぶデザインパターン〜Strategy編〜
- TypeScriptで学ぶデザインパターン〜Composite編〜
- TypeScriptで学ぶデザインパターン〜Decorator編〜
- TypeScriptで学ぶデザインパターン〜Visitor編〜
- TypeScriptで学ぶデザインパターン〜Chain of Responsibility編〜
- TypeScriptで学ぶデザインパターン〜Facade編〜
- TypeScriptで学ぶデザインパターン〜Mediator編〜
- TypeScriptで学ぶデザインパターン〜Observer編〜
- TypeScriptで学ぶデザインパターン〜Memento編〜
- TypeScriptで学ぶデザインパターン〜State編〜
- TypeScriptで学ぶデザインパターン〜Flyweight編〜
- TypeScriptで学ぶデザインパターン〜Proxy編〜
- TypeScriptで学ぶデザインパターン〜Command編〜
Commandパターンとは
命令(あるメソッドを呼び出すといったお仕事)をクラスとして表現するためのパターンです。
サンプルコード
Commandパターンで作られたクラス群がどんなものになるのか確認していきましょう。
今回は、題材として"ある数値を2乗する簡単な機能"を想定します。GitHubにも公開しています。
modules/Command.ts
命令を表現するインターフェースです。
Command.tsimport Receiver from "./Receiver"; export default interface Command { setReceiver(receiver: Receiver): void; execute(): void; }
setReceiver
では、命令の受け取り手を設定します。
execute
では、実際に命令を行います。命令の詳細は実装クラスのところで解説します。modules/ConcreteCommand.ts
命令を表現するクラスです。
ConcreteCommand.tsimport Command from "./Command"; import Receiver from "./Receiver"; export default class ConcreteCommand implements Command { private number: number; private receiver: Receiver; constructor(number: number) { this.number = number; } setReceiver(receriver: Receiver): void { this.receiver = receriver; } execute(): void { console.log(this.receiver.action(this.number)); } }
setReceiver
では、命令の受け取り手を設定します。
execute
では、実際に命令を行います。命令の受け取り手の処理を呼び出します。modules/Receiver.ts
命令の受け取り手を表現するクラスです。
Receiver.tsexport default class Receiver { action(number: number): number { return number * number; } }
action
では累乗した数値を返却します。modules/Invoker.ts
命令の実行を管理するためのクラスです。
Invoker.tsimport Command from "./Command"; export default class Invoker { private commands: Command[] = []; addCommand(command: Command): void { this.commands.push(command); } execute(): void { for (const command of this.commands) { command.execute(); } } }
addCommands
では、管理する対象のコマンドを追加します。コマンドを複数追加できる仕組みのおかげで過去のコマンドを呼び出したりコマンドを実行し直すことができます。
execute
では、コマンド群を順番に実行します。Main.ts
本パターンで作成したクラス群を実行する処理です。
Main.tsimport Receiver from "./modules/Receiver"; import Invoker from "./modules/Invoker"; import Command from "./modules/Command"; import ConcreteCommand from "./modules/ConcreteCommand"; const receiver: Receiver = new Receiver; const invoker: Invoker = new Invoker; const threeCommand: Command = new ConcreteCommand(3); threeCommand.setReceiver(receiver); const fiveCommand: Command = new ConcreteCommand(5); fiveCommand.setReceiver(receiver); invoker.addCommand(threeCommand); invoker.addCommand(fiveCommand); invoker.execute();クラス図
ここまでCommandパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Commandパターンの全体像を整理するのにお役立てください。
- Command: サンプルコードでは
Command
インターフェースが対応- ConcreteCommand: サンプルコードでは
ConcreteCommand
クラスが対応- Receiver: サンプルコードでは
Receiver
クラスが対応- Invoker: サンプルコードでは
Invoker
クラスが対応- Client: サンプルコードでは
Main
が対応※LucidChartを使用して作成
解説
最後に、このデザインパターンの存在意義を考えます。
命令をクラスとして持たせると履歴を管理したり過去のコマンドを再度実行したりといった管理を行うことができます。こういった命令自体を管理したいときに便利なデザインパターンとなります。
補足
サンプルコードの実行方法はこちらと同様です。
参考
あとがたり
なかなか使い所が難しい。
- 投稿日:2020-05-28T21:37:25+09:00
【BootstrapVueコピペのみ】導入から画像一覧画面の実装まで
Vueバージョン確認
npm list vueまずは上記コマンドでバージョンの確認
twinzlabo@0.1.0 /Users/twinzlabo ── vue@2.6.11BootstrapVueの導入
BootstrapVueの導入がまだの方のために念のため導入方法書いときますね
とりあえずコピペして環境を整えてください
main.jsimport BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' Vue.use(BootstrapVue)npm install vue bootstrap-vue bootstrap以上でBootstrapVueの導入は完了です
画像一覧画面の実装
説明は抜きにしてコードを下に貼ってあるのでどんどんコピペして
自分のプロジェクトに合った修正を加えてみてください
一応先に完成イメージです(モバイルに合わせてコーディングしてます)
<template> <b-container> <b-row> <b-col> <div v-for="(item, i) in items" class="images" :key="i"> <b-img thumbnail fluid :src="item.imageUrl"></b-img> </div> </b-col> </b-row> </b-container> </template><script> export default { data () { return { items: [ { imageUrl: require('@/assets/images/1.png') }, { imageUrl: require('@/assets/images/2.png') }, { imageUrl: require('@/assets/images/3.png') }, { imageUrl: require('@/assets/images/4.png') }, { imageUrl: require('@/assets/images/5.png') }, { imageUrl: require('@/assets/images/6.png') }, { imageUrl: require('@/assets/images/7.png') }, { imageUrl: require('@/assets/images/8.png') }, { imageUrl: require('@/assets/images/9.png') }, { imageUrl: require('@/assets/images/10.png') } ] } } } </script>いかがでしたでしょうか?
見た目はこれからですが画像一覧画面にはなったかと思います以上です
こちらの記事にてstyleのコードまで詳しく参照できます
【Vue/BootstrapVueコピペのみ】Bootstarap導入からシンプルな画像一覧画面の実装方法までを徹底解説
- 投稿日:2020-05-28T21:22:03+09:00
[Javascript]繰り返し処理(while, for)
while
while (条件式) {
処理
}let number = 1; while (number < 10) { console.log(number); number += 1; }for
for (変数定義;条件式;変数の更新) {
処理
}for (let number = 1; number < 10; number += 1) { console.log(number); }if文との組み合わせ
for (let number = 1; number < 10; number += 1) { if (number % 2 === 0) { console.log("2の倍数です"); } else { console.log(number); } }
- 投稿日:2020-05-28T19:50:24+09:00
JavaScriptを用いて非同期通信実装②
- 投稿日:2020-05-28T19:39:50+09:00
ロシア農民の掛け算をJavaScriptでやってみた
ロシア農民の掛け算
いわゆる「ロシア農民の掛け算」をJavaScriptで実装してみました。
動作原理は、ネット上にもいろいろ詳しい解説がありますが、下記のような感じです。
例題:34227 * 36070
片方を2倍、片方を半分にする。
34227 * 36070
= (34227 * 2) * (36070 / 2)
= 68454 * 180352.を繰り返していくのが基本だが、半分にする方の数が奇数だった場合はそのままでは半分にならないので、以下のように変形する。
68454 * 18035
= 68454 * (1 + 18034)
= 68454 + 68454 * 18034
= 68454 + ((68454 * 2) * (18034 / 2))
= 68454 + 136908 * 9017136908 * 9017 の部分を3.と同様の手順で変形する。
136908 * 9017
= 136908 + 136908 * 9016
= 136908 + 273816 * 4508今度は 273816 * 4508 を2.と同様の手順で変形する。
273816 * 4508
= (273816 * 2) * (4508 / 2)
= 547632 * 2254半分にする方の数が 1 になるまで、2. 3. を繰り返す。
変形後の式は足し算のみになっているので、足し算を実行する。
68454 + 136908 + 1095264 + 2190528 + 4381056 + 35048448 + 70096896 + 1121550336
= 1234567890ソース
RussianPeasant.html<html> <head> <meta http-equiv="Content-Type" context="text/html; charset=UTF-8" /> <meta charset="UTF-8" /> <title>ロシア農民の掛け算。</title> <script> function doCalc() { var txtN1 = document.getElementById("txtN1"); var txtN2 = document.getElementById("txtN2"); var n1, n2, strResult; var vWork = []; var vOut = []; n1 = parseInt(txtN1.value, 10); n2 = parseInt(txtN2.value, 10); vOut.push(" " + n1 + " * " + n2); while (1) { if (n2 <= 1) { break; } if (n2 % 2 == 0) { n2 = n2 / 2; } else { strResult = "= "; for (let i = 0; i < vWork.length; i++) { strResult += vWork[i] + " + "; } vOut.push(strResult + n1 + " + " + n1 + " * " + (n2 - 1)); n2 = (n2 - 1) / 2; vWork.push(n1); } n1 = (n1 * 2); strResult = "= "; for (let i = 0; i < vWork.length; i++) { strResult += vWork[i] + " + "; } if (n2 == 1) { vOut.push(strResult + n1 + " * " + n2); vOut.push(strResult + n1); } else { vOut.push(strResult + n1 + " * " + n2); } } var nResult = 0; for (let i = 0; i < vWork.length; i++) { nResult += vWork[i]; } nResult += n1; vOut.push("= " + nResult); document.getElementById("result").innerHTML = vOut.join("\n"); } </script> </head> <body> <h2>ロシア農民の掛け算。</h2> 5桁 * 5桁 まで<br /> <input type="text" id="txtN1" value="34227" maxlength="5" size="7" oninput="value = value.replace(/[^0-9]+/i,'');" /> × <input type="text" id="txtN2" value="36070" maxlength="5" size="7" oninput="value = value.replace(/[^0-9]+/i,'');" /> <input type="button" value="計算" onclick="doCalc()" /><br /> <br /> <textarea id="result" rows="50" col="20" style="width:700px;height:400px;"> </textarea> </body> </html>
- 投稿日:2020-05-28T19:21:32+09:00
【Vue/Font Awesome導入時のエラー解決】error There should be no space after this paren space-in-parens
Vueバージョン確認
npm list vueまずは上記コマンドでバージョンの確認
twinzlabo@0.1.0 /Users/twinzlabo ── vue@2.6.11エラーメッセージ
Failed to compile. ./src/main.js Module Error (from ./node_modules/eslint-loader/index.js): /Users/twinzlabo/src/main.js 17:15 error Multiple spaces found before ”font-awesome-icon” no-multi-spaces 17:15 error There should be no space after this paren space-in-parens 17:53 error Multiple spaces found before ‘)’ no-multi-spaces 17:53 error There should be no space before this paren space-in-parens解決策
今回のエラー文で注目すべき箇所は
17:15 error Multiple spaces found before ”font-awesome-icon” no-multi-spaces 17:15 error There should be no space after this paren space-in-parens 17:53 error Multiple spaces found before ‘)’ no-multi-spaces 17:53 error There should be no space before this paren space-in-parensつまりmain.js17行目でスペースがおかしい使い方されているよというエラーです
ではまず実際のエラー箇所を確認してみましょう
main.js// about fontawesome--- import { library } from '@fortawesome/fontawesome-svg-core' import { faCoffee, faSpinner, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' // ------------------- import App from './App.vue' import router from './router' import store from './store' library.add(faCoffee, faSpinner, faAngleDoubleUp) Vue.component( 'font-awesome-icon', FontAwesomeIcon )どうやらFont Awesome導入時にエラーが発生してしまったようですね
Vue.component( ‘font-awesome-icon’, FontAwesomeIcon )こちらぱっと見は全然問題なさそうですが、
‘font-awesome-icon’, FontAwesomeIconこの前後にスペースが1つずつ存在してしまっています
細かいですがESlintではこの程度でもエラーを表示してしまうので気をつけましょう
修正後のコードはこのようになります
// about fontawesome--- import { library } from '@fortawesome/fontawesome-svg-core' import { faCoffee, faSpinner, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' // ------------------- import App from './App.vue' import router from './router' import store from './store' library.add(faCoffee, faSpinner, faAngleDoubleUp) Vue.component('font-awesome-icon', FontAwesomeIcon)これで無事解決しましたね
以上です
参考記事
【Vue/Font Awesome導入】Font Awesomeを導入してアイコンを使用するまでの流れを徹底解説
- 投稿日:2020-05-28T19:09:57+09:00
【jQuery】ぺージのスクロールボタンを作りたい
ページの一番上や、特定の位置にスクロールさせるボタンを設置する際の
簡潔な手順をご紹介。1. htmlでボタンを準備
今回はフッターの中に設置しました。
sample.html<div class="footer"> <div class="footer__btn"> Page Top </div> </div>2. 位置を固定する
スクロール中に隠れないように、「z-index」も調整しておきます。
sample.css.footer__btn { cursor: pointer; height: 80px; width: 80px; z-index: 10; position: fixed; bottom: 20px; right: 32px; background-color: red; line-height: 80px; text-align: center; font-size: 20px; }3. jsを準備する
ボタンのクリックイベントによって、ページの最上部までスクロールさせます。
sample.jsvar scrollTop = window.pageYOffset ; //スクロール量を取得 $(".footer__btn").on("click", function(){ //クリックした時に発火 $( 'html,body' ).animate( {scrollTop:0}, 'slow' ); //ゆっくり一番上までスクロールさせる }) $(".gy-btn").on('click', function(){ $( 'html,body' ).animate( {scrollTop:1500}, 'slow' ); })
ヘッダーのボタン等で指定位置までスクロールさせたい場合は
scrollTopの値を変更します。sample.jsvar scrollTop = window.pageYOffset ; $(".other-btn").on('click', function(){ $( 'html,body' ).animate( {scrollTop:1500}, 'slow' ); })
以上で終了です。
ご覧いただきありがとうございました。
- 投稿日:2020-05-28T18:22:23+09:00
webpack 5 Module Federationにおけるchunk最適化
(※これからかくことは次回リリースで変更される可能性もあります)
Module Federation概要
webpack 5でModule Federationという仕組みが入る。
例えば、以下のように書くと、
app_01
のshred
オプションに記述したモジュールが、app_02
のアプリに対して共有されるようになる。plugins: [ new ModuleFederationPlugin({ name: "app_01", library: { type: "var", name: "app_01" }, filename: "remoteEntry.js", remotes: { app_02: "app_02", }, exposes: { SideNav: "./src/SideNav", Page: "./src/Page" }, shared: ["react", "react-dom", "@material-ui/core", "react-router-dom"] }), ]plugins: [ new ModuleFederationPlugin({ name: "app_02", library: { type: "var", name: "app_02" }, filename: "remoteEntry.js", remotes: { app_03: "app_03", }, exposes: { Dialog: "./src/Dialog", Tabs: "./src/Tabs" }, shared: ["react", "react-dom", "@material-ui/core", "react-router-dom"] }), ]<!DOCTYPE html> <html lang="en"> <head> <!-- app_02 --> <script src="http://localhost:3002/remoteEntry.js"></script> </head> <body> <div id="root"></div> <!-- app_01 --> <script src="http://localhost:3001/main.js"></script> <script src="http://localhost:3001/remoteEntry.js"></script> </body> </html>
app_02
のモジュールは非同期に読み込まれ、実行時にchunkを解決する。実行するまで不明なプログラムであるはずなので、エラーなどもその時までわからない。しかし、逆に言えば、
app_01
とapp_02
のビルドパイプラインは疎結合にできるので、Micro-Frontendsにおいて活用できそう、というわけだ。overridablesとoverrides
ModuleFederationPlugin
では、ContainerPlugin
、ContainerReferencePlugin
というクラスを内部で扱っている。詳細はこちらのとおりだが、それぞれがoverridables
、overrides
というオブジェクトを提供している。
overridables
-remote
として読み込まれたとき、local
によって上書き可能なモジュールoverrides
-local
として読み込まれたとき、remote
を上書きするモジュールこれをハッシュで識別し、実行時に解決・最適化するのがModule Federationの実態だ。前述の例では、
shared
オプションに指定されたモジュールが上書き対象となっている。sharedの問題
しかし、
shared
オプションをつかったchunkにはひとつ問題がある。
ModuleFederationPlugin
でlocal
とremote
それぞれのコードを生成する箇所は以下のようになっている。compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => { if ( options.exposes && (Array.isArray(options.exposes) ? options.exposes.length > 0 : Object.keys(options.exposes).length > 0) ) { // remote 用のコード new ContainerPlugin({ name: options.name, library: options.library || compiler.options.output.library, filename: options.filename, exposes: options.exposes, // options.shared その1 overridables: merge(options.shared, options.overridables) }).apply(compiler); } if ( options.remotes && (Array.isArray(options.remotes) ? options.remotes.length > 0 : Object.keys(options.remotes).length > 0) ) { // local 用のコード new ContainerReferencePlugin({ remoteType: options.remoteType || (options.library && options.library.type) || compiler.options.externalsType, remotes: options.remotes, // options.shared その2 overrides: merge(options.shared, options.overrides) }).apply(compiler); } });これをみると、
options.shared
は2箇所でつかわれていることがわかる。つまり、shared
を使うと、overrides
とoverridables
を同時に提供してしまうので、アプリのユースケースよっては不要なコードが生成されてしまうのだ。具体的なパターンをみてゆく。
overridablesのみ提供したいパターン
例えば、
app_02
を以下のように仮定する。
app_02
はremote
として参照されることがあり、いくつかのライブラリをlocal
(app_01
)に上書きしてほしいapp_02
からremote
(app_03
)を呼ぶケースでは、app_02
とremote
で共通するライブラリがない- この場合「
app_02
はoverridables
のみを提供し、overrides
を提供することはない」ということになる。
shred
を使うと、local
用のビルドでoverrides
のchunk
もつくってしまう。しかし、前述のような想定だと、overrides
のchunk
が使用されるケースはない。これを解決するのが
overridables
オプションである。shared
の代わりにoverridables
と明示的に指定することで、overrides
が生成されることはなくなる。plugins: [ new ModuleFederationPlugin({ name: "app_02", library: { type: "var", name: "app_02" }, filename: "remoteEntry.js", remotes: { app_03: "app_03", }, exposes: { Dialog: "./src/Dialog", Tabs: "./src/Tabs" }, // 変更箇所 overridables: ["react", "react-dom", "@material-ui/core", "react-router-dom"] }), ]実際には、以下のようにchunkを削除できる。
overridesのみ提供したいパターン
同様に、例えば、
app_01
を以下のように仮定してみる。
app_01
はapp_02
をremote
として参照し、いくつかのライブラリを上書きしたいapp_01
はライブラリのバージョンが古いため、remote
として参照される場合に、ライブラリを上書きされたくない- この場合「
app_01
はoverrides
のみを提供し、overridables
を提供することはない」ということになる。
shred
を使うと、remote
用のビルドでoverridables
のchunk
をつくってしまう。前述のような想定だと、overridables
のchunk
が使用されることがないし、また、生成されるべきでもない。これは
overrides
オプションで解決できる。shared
の代わりにoverrides
を使うことで、overridables
が生成されることはなくなる。plugins: [ new ModuleFederationPlugin({ name: "app_01", library: { type: "var", name: "app_01" }, filename: "remoteEntry.js", remotes: { app_02: "app_02", }, exposes: { SideNav: "./src/SideNav", Page: "./src/Page" }, // 変更箇所 overrides: ["react", "react-dom", "@material-ui/core", "react-router-dom"] }), ]実際には、以下のようにchunkを削除できる。
雑感
- 実際はこれらを手で管理してゆくのは厳しくないか・・?
- アプリケーションのユースケースと責務がはっきりしていれば、明示的に指定することはできるかもしれない
- 最初から意識しておけば
shared
を使うことこと自体は減りそう- automatic-vendor-federationというプラグインがあるが、現状は
shared
しか対応していない- chunkが生成されてしまうだけで、fallbackもあるのでバグるわけではない。あくまで最適化したい場合
- 脳死
shared
でもそんなには困らないのかも参考
- 投稿日:2020-05-28T18:21:07+09:00
console.logでフォーマット指定子を使う!
console.log()
JavaScriptの
console.log()
でC言語のようなフォーマット指定子が使えることを知ったので、まとめておきます。通常
とりあえず通常の書き方
console.log("hoge")
%s
ただの文字列
いまいち使い道が分かりません
console.log("%s", hoge)
しいて言うならこれですかね。
...arry
はスプレッド構文という書き方で、配列やオブジェクトを展開してくれます。%oまたは%O
つまずきがちなObjectの出力
consol.log("%o", hoge)
%dまたは%i
お次は整数値です。
console.log("%d", hoge)
0埋めや半角スペース埋めもできます。
これはChromeやSafariでは使用できませんでした。
下記画像はFirefoxでの検証結果です。
ちなみに小数を表示しようとすると、小数点以下は切り捨てられます。
入力: 1.1 → 出力: 1
入力: -1.1 → 出力: -2%f
浮動小数点です。
console.log("%f", hoge)
小数点以下の桁数指定ができます。
これもChromeやSafariでは使用できなかったので、Firefoxでの検証結果です。
%c
出力にCSSスタイルを適用できます。
console.log("%cあいうえお", style)
- 投稿日:2020-05-28T16:45:51+09:00
【神業】面倒なHTMLタグ入力を光の速さで終わらせる【定型文ツール】
はじめに
フリーランスRailsエンジニアおよびWEBライターをやっているソエノと申します。この記事では「面倒な定型文入力を神速で終わらせる方法」についてご紹介します。
注意事項 : コメント欄への書き込みについて
コメント欄への「誹謗中傷・暴言」の書き込みはインターネット上であれ「威力業務妨害」が成立する危険性があります。全てのコメントは削除後も筆者のメールアドレスに保存されていますので、何卒ご注意の程よろしくお願いいたします。
結論
「Dash」というツールを使います。以下のようなことができます。以下は、HTMLのaタグを書くときの例です。
う〜ん、神。普通、aタグを書こうとしたら
<a href="hoge">fuga</a>
のhogeとfuga以外の部分って手入力しますよね。上記の動画では;atag
という呪文によって謎のウィンドウを召喚し、hogeとfugaだけを入力してEnterすることによって何とaタグを完成させてしまいました。変換キーとかも使ってないので、もはや辞書登録使うより速いです。このツールを使うなら、ブロガーやフロントエンジニアの方はHTMLタグを書くために
<a href="hoge">fuga</a>
みたいなテキストを手入力するのは金輪際やめましょう。Railsエンジニアなら「routes.rbのresourcesってどういう風に書くんだっけ...」とか「バリデーションってどう書くんだっけな?ググるか...」みたいなことが今後一切なくなります。ダウンロード方法
こちらからダウンロードできます。リンク先にアクセスしたら、画面右にあるDownloadをクリックです。zipファイルがダウンロードされるので、解凍してアプリケーションフォルダにアプリを移動させましょう。
使い方
使う前に、アプリの動作許可設定しないと動かないのでMacのシステム環境設定を開きましょう。パソコン左上に表示されているりんごマークをクリックしたら「システム環境設定」というのがありますから選びます。
家のマークで「セキュリティとプライバシー」というのがありますので、これを選びます。下記画面で左側のウィンドウを少し下にスクロールすると、「アクセシビリティ」という項目があるので、選びます。そして左下の錠マークをクリックしてパソコンのロックを解除します。あとは「Dash」というところのチェックをONにしましょう。これで準備OKです。
それではDashを起動しましょう。
起動後の画面はこんな感じです。まずは右上の「Manage Docsets」をクリックしましょう。
上記のような画面になります。続けて、上のメモマーク「Snippets」を選択しましょう。
Snippetsを選択したら上記のような画面になるので、Enable snippetsというチェックをONにしましょう。ONにしたらこの設定画面は不要なので左上の×マークで閉じます。
設定後、ホーム画面のSearchという欄でSnippetsというのが選択できるようになりますので、クリックします。
すると上記のようになるので、New Snippetsをクリック。
なんか入力欄が二つ出てきます。上の細い入力欄が、先ほど紹介した「呼び出しの呪文」です。下の大きなの欄が「実際に呼び出されるテキスト」になります。試しに、次のように設定してみましょう。上の入力欄には
;atag
、下の入力欄には<a href="__link__">__text__</a>
と書きました。設定はこれで完了です。特に保存とかは無いみたいなので、このままDashアプリからは離れてしまって構いません。あとは、適当なテキスト入力欄に
;atag
と打ってみてください。なんか召喚されると思います。新しいスニペットを登録する
この「スニペット」というのが先ほどのように「召喚の呪文と、そこから召喚されるテキストの組み合わせ」という意味のようです。新しいスニペットを登録するには、下記動画のように画面上のプラスマークをクリックすればいいようです。
さらに高速化していく方法【カスタマイズ機能】
上記では
__link__
や__text__
といった記述によってテキストを入力できるようにカスタマイズしました。これは「アンダースコア2つで囲むと任意テキスト入力できるようにする」というDashのカスタマイズ機能です。このほかにも便利な機能がいくつもありますので、使えそうなものをピックアップしてご紹介します。クリップボードの情報を自動で挿入する
ここでいうクリップボードというのは「コピーしたテキスト」のことです。例えば、さっきのaタグ生成のやつを以下のように書くと、直前にURLをコピーしていれば、コピーしたテキストを勝手にペーストしてくれます。
@clipboard
という記述がそのままクリップボードの情報に置き換わるようです。挿入後のカーソルの位置を指定する
さっきはDashで呼び出されたウィンドウ上で文字入力していましたが、先にaタグを生成してからエディタ上でテキストを入れたい時はこれが便利です。
@cursor
と書くと、スニペット挿入後に勝手にカーソルが@cursor
と書いた位置に飛んでくれます。便利なプリセット
この記事をここまで呼んでくださった方のために、プリセットをご用意してみました。よろしければ、ご活用ください。
HTML
aタグ;atag <a href="@clipboard">@cursor</a>divタグ;divtag <div class="__class__">@cursor</div>hタグ(h1,h2,h3,...);htag <h__number__>@cursor</h__number__>pタグ;ptag <p>@cursor</p>scriptタグ(JavaScript埋め込み)<script type="text/javascript"> @cursor </script>任意のHTMLタグ;html <__html__>@cursor</__html__>Ruby on Rails
routes.rbのresources;resources resources :__controllerName__, only: [:__action1__, :__action2__]form_with(haml記法);form_with = form_with model: __modelName__, url: __actionUrl__, local: __trueOrFalse__ do |f| @cursorバリデーション(存在);validate validates :__columnName__, presence: trueJavaScript
console.log;console console.log("@cursor");jQueryを使ったDOM取得;jq $("@cursor")記事を読んでいただきありがとうございました
ここまで読んでくださって本当にありがとうござます。私から皆さんにGiveさせて頂けるのはこうしたエンジニア向け情報くらいしかありませんが、皆さんに記事を読んでいただけて、そして皆さんのお役に立つことができれば、それは私の何よりの喜びになります。今後とも皆さんのお役に立てるよう記事執筆活動などを進めていきたいと思っていますので、これからも何卒よろしくお願いいたします。
まとめ
・Dashというアプリを使うことで、HTMLタグの入力を楽にしたり、Railsなどのプログラミング言語における「定型句」を覚える必要がなくなりました。代わりに自分で登録した「召喚の呪文」を覚える必要がありますが、これまでの作業よりも幾分マシになっているはずです。
以上となります。それでは、生産的なコーディングライフを!!
筆者Twitter: soeno_onseo
- 投稿日:2020-05-28T16:45:10+09:00
function式とarrow式の違いをサッカー選手を呼び出しながら説明するよ
概要
function式とarrow式の違いをサッカー選手を使いながら軽く説明するよ
問題
早速、問題を出します。
aとbの違いわかりますか?const a = function() { console.log('neymar') } const b = () => { console.log('neymar') }この場合だとbのほうが合計文字数がすくなくなったくらいですかね。
次
// let, const であればトップレベルで宣言してもwindowオブジェクトのプロパティを生成しませんが今回は問題の都合上varでいきます // よほどのことがない限りこんな書き方をしないけどグローバルのnameに代入するために記載 // var宣言が怖い理由の一つもこれですね。 var name = '??global-messhi??' const printName = function() { console.log('printName', this.name) } const arrowPrintName = () => { console.log('arrowPrintName', this.name) } const obj1 = { name: 'nagatomo', printName } const obj2 = { name: 'king-kazu', arrowPrintName } // 出力される名前はなんでしょう obj1.printName() obj2.arrowPrintName()
答え
obj1.printName() // nagatomo obj2.arrowPrintName() // ??global-messhi??
アロー関数式とfunction式の違い
先程の問題の結果を見て、文字数が短くなっただけではないことはすでにわかったと思います。
this
の指している場所が違います。通常のfunction式の中でthisを使うと、その呼び出し元のオブジェクトを指します。
var name = '??global-messhi??' const printName = function() { console.log('printName', this.name) } const obj1 = { name: 'nagatomo', printName } const obj2 = { name: 'king-kazu', printName } obj1.printName() // 'nagatomo' obj2.printName() // 'king-kazu'なのでこの場合printName関数内で使っている
this.name
の値は呼び出し元がobj1とobj2なのかで値が異なってきます。
function式だとthisが呼ばれたタイミングで決定されちゃんと自立できていない人のような振る舞いです。
なのでfunction式は直感でわかりにくいので苦労します。アロー関数は結論から言うと
this
は宣言された時点で、this
を確定します。var name = '??global-messhi??' const arrowPrintName = () => { console.log('arrowPrintName', this.name) // この時点でのthisはグローバルになる } const obj1 = { name: 'nagatomo', arrowPrintName } const obj2 = { name: 'king-kazu', arrowPrintName } obj1.arrowPrintName() // '??global-messhi??' obj2.arrowPrintName() // '??global-messhi??'なので、呼び出し元がobj1だろうが、obj2だろうが、関係なくて直感的にthisの指している場所がわかります。
今学んだことを再確認するために、もっかい問題出します。
class Person { constructor(name) { this.name = name } printName = function () {console.log('printName', this.name)} arrowPrintName = () => {console.log('arrowPrintName', this.name)} } const printName = function() { console.log('printName', this.name) } const arrowPrintName = () => { console.log('arrowPrintName', this.name) } class Person2 { constructor(name) { this.name = name } printName = printName arrowPrintName = arrowPrintName } const kakitani = new Person('kakitani') const maezono = new Person2('maezono') // 出力される名前はなんでしょう kakitani.printName() kakitani.arrowPrintName() maezono.printName() maezono.arrowPrintName()
答え
kakitani.printName() // kakitani kakitani.arrowPrintName() // kakitani maezono.printName() // maezono maezono.arrowPrintName() // ??global-messhi??
次
const bindPrintName = function() { console.log(this.name) }.bind(this) // function式をarrow関数と同じ感じでthisをつかえるようにするための魔法のコトバだよ class Person3 { constructor(name) { this.name = name } bindPrintName = bindPrintName } // 出力される名前はなんでしょう const zaccheroni = new Person3('zaccheroni') zaccheroni.bindPrintName()
答え
const zaccheroni = new Person3('zaccheroni') zaccheroni.bindPrintName() // ??global-messhi??
Reactでよくあるパターン
これだとボタンを押した時
Uncaught TypeError: Cannot read property 'state' of undefined
が出ます。
理由わかりますか?export default class Hoge extends Component { constructor(props) { super(props); this.state = { name: 'zico' }; } handlePrintName() { const { name } = this.state; return name } render() { const { name } = this.state; return ( <button onClick={this.handlePrintName}> print name </button> ); } }解決方法
handlePrintName()
をアロー関数にする// 変更前 handlePrintName() { // これでthisが定まっていない const { name } = this.state; return name } // 変更後 handlePrintName = () => { // これでthisをHogeにbind const { name } = this.state; return name }ただReactの場合これには問題点があります。
レンダー内でアロー関数を利用するとコンポーネントがレンダーされるたびに新しい関数が作成されるため、子コンポーネントでReact.memoやPureComponentを使ってた場合に正しく比較されなくなる
ClassComponentでは
bind(this)
を使う場合が多いexport default class Hoge extends Component { constructor(props) { super(props); this.state = { name: 'zico' }; this.handlePrintName = this.handlePrintName.bind(this) } handlePrintName() { const { name } = this.state; return name } render() { const { name } = this.state; return ( <button onClick={this.handlePrintName}> print name </button> ); } }参考記事
JavaScriptの「this」は「4種類」??
JavaScript の this を理解する多分一番分かりやすい説明
- 投稿日:2020-05-28T16:37:57+09:00
kintone ポータルをアプリで動的に制御
github repositoryはこちらから。 見た目 仕様 ポータルの表示内容を動的にアプリのレコードで制御をすることができます。 初心者の方も簡単に利用できるようになってます。 注意: この自動ポータル機能の設定には管理者権限で、 kintone全体カスタマイズにjsファイルをアップロードする必要があります。 REST APIを使い、アプリからポータルへ、レコード取得をしているので ユーザーが多い環境では1日1万件のリクエスト上限を超え、 ポータルカスタマイズが利用できなくなる可能性もあります。 導入の流れ アプリテンプレートからアプリ作成 レコード作成 JSの一部を変更 jQueryと一緒に全体カスタマイズ保存 githubから関連ファイルをダウンロード githubにアクセスして、 右上の↓CodeからDownload Zipでファイルをダウンロード。 ダウンロードできたら、zipファイルを展開します。 (エクスプローラー上で右クリック→すべて展開) Portal_Setting アプリ作成 アプリは全員が閲覧権限を持つスペース、 また、そのようなスペースが存在しない場合は、 ポータルトップ上でアプリの作成をして下さい。 先ほど、展開したファイルの中にtemplatesというファイルがあります。 その中のportal_manage.zipをアプリ作成の、 テンプレートファイルを読み込んで作成に使用します。 設定 アプリ名は変えていただいて問題ありません。 アプリの設定はテンプレートから作成のままで変更は特にありません。 入力制御も最低限、プログラムで補っています。 フィールドコード、フィールド名はプログラムに使用している箇所もあります。 不必要に変えないようにしてください。 一覧はデフォルトで見やすいように設定しているだけなので、 絞り込みなどを参考に追加、作り直し問題ありません。 閲覧権限 ログインユーザーによってポータルの表示内容変更ができます。 アプリショートカット、スペースの表示は、 kintone従来のレコードの閲覧権限で制御できます。 レコードの閲覧権限についてはこちらから。 レコード登録 ポータル背景 お知らせ アプリショートカット スペース 上記の4項目を設定します。 ポータル背景 1度データ作成した後に変更する場合はデータを編集してください。 フィールド 内容 ポータル背景_画像 ポータルの背景になる画像ファイル アプリ群タイトル アプリショートカット枠のタイトル お知らせタイトル お知らせ枠のタイトル スペースタイトル スペース枠のタイトル お知らせ一覧 お知らせ内容はリッチエディタの内容がHTML形式でトップに表示されます。 優先順位は更新日時が一番最新のものが1件表示されるようになっています。 お知らせの履歴を残すこともできますが、 レコード数が多くなればなるほど、ポータルの表示速度が遅くなる可能性があります。 推奨はレコード編集で随時変更する運用です。 フィールド 内容 お知らせ内容 リッチエディタ アプリショートカット 設定数制限はありませんが、ショートカットなのでスマートに使いましょう。 メイン項目 アイコンファイルは正方、(100px X 100px等)を推奨しています。 ファイルは1つまでです。 フィールド 内容 アプリ優先順位 ポータルに表示する並びの指定 アプリID アプリID、URL指定に使いますアプリトップのhttps://sample.cybozu.com/k/XXXXの数字 アプリアイコン アプリアイコン画像ファイル デザイン 少し難しいかもしれませんが、デザインの幅を広げるため、 グラデーションカラー設定を可能にしています。 詳細はこちら。 カラーの指定方法(例) #ffce44 100% またはrgb rgb(255,0,0) 100% フィールド 内容 数値 グラデーションの傾き設定 カラー1 指定方法: (カラーコード[半角スペース]濃さ%) カラー2 指定方法: (カラーコード[半角スペース]濃さ%) 例: linear-gradient(180deg,#F4CC70 0%,#DE7A22 100%) 以下のテーブル内容で上記のCSSがアプリアイコンのbackgroundに適用されます。 フィールド 内容 数値 180 カラー1 #F4CC70 0% カラー2 #DE7A22 100% スペース フィールド 内容 スペース優先順位 ポータルに表示するスペースの並びの指定 スペースID スペースID、URL指定に使いますスペーストップのURLhttps://sample.cybozu.com/k/#/space/XXXXの数字 スペース名 スペースの名前(実際のスペースの名前と違っても、問題ないです。) jsファイルの編集 ダウンロードしたファイルのtemplatesのjsフォルダ中に、 ~~~desktop.js, ~~~mobile.js というファイルがあります。 初心者の方 右クリックで編集を選択後、警告を確認して開いてください。 下記の1の数字を先ほど作成したアプリのアプリIDを半角数字で入れる (function() { "use strict"; //半角数字で作成したアプリのIDを設定 const APPID = 1; ----------------以下省略------------------ 例:アプリのページを開いた時のurlが https://xxxx.cybozu.com/k/666/ の時、 666; セミコロン;は消さないでください。 (function() { "use strict"; //半角数字で作成したアプリのIDを設定 const APPID = 666; ----------------以下省略------------------ 今回のカスタマイズで必要なもの 全体カスタマイズ: PC用のJavaScriptファイル https://js.cybozu.com/jquery/3.5.0/jquery.min.js Desktop用ファイル スマートフォン用のJavaScriptファイル https://js.cybozu.com/jquery/3.5.0/jquery.min.js Mobile用ファイル コピペURLを追加をして大丈夫です。 kintoneポータル設定 ポータル右上の設定からポータル設定ができます。 ポータルコンテンツの表示に関しては、どの設定でも問題はありません。 ポータルを整理するのであれば、通知と未処理の項目くらいがベスト。 参考 kintone help レコードの閲覧権限を設定する Kintone Portal Designerを使ってポータルをデザインしよう MDN web docs linear-gradient()
- 投稿日:2020-05-28T15:08:22+09:00
Yellowfinのv9ダッシュボードでレポートの値からフィルターを制御する
概要
前回の記事ではレポートの値をフォームに入れてPOSTしましたが、今回は応用でレポートの値でフィルターを効かせるようにします。
表示するレポートを選ぶ
今回は、事前にフィルター付きのレポートを用意します。
ここでは、YellowfinのチュートリアルデータであるSkiTeamのデータにあるCamp Performanceのレポートを元にしてCamp Regionのカラムをフィルターにセットします。(演算子:一覧に含む、値:ユーザープロンプト、フィルターの書式→表示→説明でフィルターの名前をfilterRegionに変更しています)
フィルターの入力スタイル
下記のように「一覧から値を選択」と表示タイプを「一覧」にします。他の表示方法だとJavascriptでフィルターとレポートの同期ができませんでした・・・・
ダッシュボードの作成
ダッシュボードを作成し、先程のレポートをドラッグアンドドロップで配置します。
フィルターも同様に、ドラッグアンドドロップします。
+フィルターボタンを押下し、ポップアップでレポートで作成したフィルターを紐つけます。
コードモードの編集
ここからコードモードに変更し、HTMLタブを見ていきます。
HTMLタブ
大体こんな感じになっていると思います。report-outputタグのnameがレポート名になっています。
dashboard.html<canvas-area xmlns="http://www.w3.org/1999/xhtml" canvas-uuid="eecfc9c0-fd84-4c5f-a4f4-692b6e8ab85b"> <report-output widget-uuid="4956314f-eac8-4563-8d65-b394fbb1ae53" report-uuid="db89f236-f4fb-4f3d-852e-9ae2e43f41aa" height="300" top="0" left="353" name="for qiita, Camp Performance" width="400" display-type="TABLE" style="z-index: 1;"></report-output> <filter-list widget-uuid="8d08b89d-d2a4-494c-8794-e7d9131e96e6" filter-list-uuid="b15da1ed-9c9b-4e00-9e5c-3ed1fca8dcb2" width="280" height="382" top="0" left="0" hide-control-panel="false" dash-filter-auto-run="false" hide-reset-link="false" name="フィルター一覧(縦)" style="z-index: 2;"></filter-list> </canvas-area>JSタブ
レポート変数にhtmlのレポートのnameを指定し、フィルター名はレポート作成時に設定した表示名をthis.apis.filters.getFilterで指定します。
jstab.jslet report = this.apis.canvas.select('for qiita, Camp Performance'); /* Use APIs to apply filters */ report.addEventListener('click', (event) => { let $row = $(event.target).closest('tr'); let demographicFilter = this.apis.filters.getFilter('filterRegion'); let text = $row.find('td').get(0).innerText; //ちなみにですが、ボタンのクリックイベントにしておいて、あらかじめtext変数を配列にしておけば、複数のフィルターの値で絞り込みができるようになります。 //let text = ['Europe', 'North America']; this.apis.filters.setFilterValue(demographicFilter.get('filterId'), text); this.apis.filters.applyFilters(); })こんな感じで
レポートの行をクリックした時にイベントが起動して、その行の一番左の値をフィルターに指定するダッシュボードが完成しました。これでより直感的なダッシュボードが作成できるようになるのではないでしょうか。
※一度完成したダッシュボードでも元のレポートやフィルターを変更したり更新したりすると、うまく動かなくなることがあるようです、、、この辺はアップデート待ちなんでしょうかね。。
- 投稿日:2020-05-28T14:52:23+09:00
Twilio Video SDKを使った時にgetUserMediaが複数回走ってしまう時の対応方法
Twilio Video SDKを使った時に、connectを呼ぶたびに
getUserMedia
が走ってユーザーに許可を求めてしまったのでその対処方法をメモ。環境
- Twilio Video SDK v2.5
やりたかったこと
最初に
navigator.mediaDevices.getUserMedia()
を実行しstreamを取得して、以後それを使いまわしたかった。起こったこと
Twilio.video.connectを呼ぶたびに、
getUserMedia
が走って、ユーザーに許可を求めてしまう。原因
Twilio.video.connectを実行した時、デフォルトだと自動でstreamを取得し、そのあとのstreamの管理も行ってくれる。
対応方法
navigator.mediaDevices.getUserMedia
で取得した、streamをconnectにオプションとして渡す。navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function(mediaStream) { return Video.connect(token, { name: 'my-cool-room', tracks: mediaStream.getTracks() // tracksでstreamを渡す }); }).then(function(room) { room.on('participantConnected', function(participant) { console.log(participant.identity + ' has connected'); }); room.once('disconnected', function() { console.log('You left the Room:', room.name); }); }).catch(error => { console.log('Could not connect to the Room:', error.message); });
- 投稿日:2020-05-28T13:13:41+09:00
ビデオチャット中に左シフトキーを押している間だけミュート解除し会話できるアプリをつくった
Google MeetやZOOMでビデオチャットをしているとき、マイクに雑音が入らないよう自分の発言中以外は基本的にミュートしているのですが、
ミュート解除を切り忘れたり、別画面を開いているとすぐにミュート解除できなかったりで、面倒だったのでキーボードの特定のキーを押している間だけミュートを解除してくれるアプリ「Cough Switch」を作成しました。トランシーバーやレコーディングスタジオのトークバック機能のようなイメージです。
AppleScriptでマイクの音量を操作する
Macの場合、
システム環境設定 > サウンド > 入力
でマイク設定を操作することができます。
この入力音量を0にするとミュートされます。applescripttell application "System Events" to set volume input volume 100このスクリプトをjsから実行するにはこんな感じ
javascriptconst applescript = require('applescript'); applescript.execString('tell application "System Events" to set volume input volume 100', function);
iohook
ですべてのキーボードイベントを取得するさくっと作りたいのでまたElectronで簡単にアプリ化していきます。
Webのいつも通りにキーボード操作を取得しようと、
document.addEventListener("keydown", function)
としてしまうと、Windowがアクティブになっていないとキーボードイベントを取得できません。
electron.globalShortcut
ではkeyup
イベントを取得できないので却下。他に探してみたところiohookってやつで取得できるみたいです。
(特定のバージョンのNodeとElectronしかサポートしていないらしくちょっとはまった)javascriptconst iohook = require('iohook'); const KEYCODE = 42; // left shift key iohook.on('keydown', (msg) => { if (msg.keycode === KEYCODE) { talk(); } }); iohook.on('keyup', (msg) => { if (msg.keycode === KEYCODE) { mute(); } });どのキーに割り当てるか悩みましたが2つあるので、左シフトキーにしました。
Electronの最前面の触れない透明なWindowを作成
ミュート解除中のアイコンを表示させるために、最前面に触れないWindowを作成します。
さらに邪魔にならないよう背景を透明にします。javascriptconst appWindow = new BrowserWindow({ transparent: true, frame: false, resizable: false, alwaysOnTop: true, }); appWindow.setIgnoreMouseEvents(true); appWindow.loadURL(`file://${__dirname}/index.html`);これで最前面にHTMLが表示されます。
あとはキーボードイベントに応じて、Windowへ状態を送り、CSSでアイコンを表示させます。javascriptappWindow.webContents.send('talk');おわり
あとは前にやった MacBook Proの充電器の情報をメニューバーに表示するElectronアプリをつくった と同様にアプリケーション化して完成です。
ソースコードはこちら(https://github.com/narikei/cough-switch)
おわりのおわり
デフォルトだとスピーカーの設定しかできないけど、
こういうときこそタッチバーの出番なのではないかな?
バックグラウンドのアプリでタッチバーを使えないのどうにかしてほしい。
- 投稿日:2020-05-28T11:18:13+09:00
ChromiumではないMS Edgeで要素ドラッグしようとするとタッチジェスチャーが動作してしまう件の回避
概要
- 最新のChromium版のMicrosoft Edgeではない、古いEdge
- タッチディスプレイで、タッチ操作
という条件で、HTML内の要素ドラッグをしようとすると、全体のタッチジェスチャー(?)が効いてドラッグできません。
上記キャプチャのURLはこちら。
https://gijgo.com/draggable解決法
CSSで、ドラッグしたい要素に
touch-action: none;
を指定するだけ!参考
- 投稿日:2020-05-28T10:50:30+09:00
jest の spy を使ったテストでテスト順番に依存して落ちたり落ちなかったりする問題の解決
spy で toBeCalled() を使ったテストで、テストの順番に依存してうまく動いたり動かなかったりする現象があって、ハマりそうだったので記録しておきます。
テスト対象は TypeScript の関数ですが、型がある以外は JavaScript に読み替えても同じだと思います。テスト対象の関数が以下のようにあるとします。
export const targetFunction = (flag: boolean) => { if (flag) { console.error('出力です') } }失敗するパターンのテスト
import { targetFunction } from '~/plugins/axios' const spyConsoleError = jest.spyOn(console, 'error') spyConsoleError.mockImplementation(() => { }) describe('spy resetの動作テスト', () => { test('console error が呼ばれる', async () => { await targetFunction(true) expect(console.error).toBeCalled() }) test('console error が呼ばれないはず', async () => { await targetFunction(false) expect(console.error).not.toBeCalled() }) })対象のテストの中では console.error は呼ばれていないはずなのですが、すでに前のテストで呼ばれているため、1回呼ばれたことになってしまっています。
うまくいくように書き換えたパターン
import { targetFunction } from '~/plugins/axios' const spyConsoleError = jest.spyOn(console, 'error') spyConsoleError.mockImplementation(() => { }) // ------ これを書き足します -------- beforeEach(() => { spyConsoleError.mockReset() }) // ------ ここまで --------- describe('spy resetの動作テスト', () => { // 同じテスト })それぞれのテストの前に beforeEach で初期化処理をしてあげると依存関係の問題が起こりません。
- 投稿日:2020-05-28T10:31:17+09:00
knex.js + postgresql migration メモ
knex.js を postgresql で利用する際の migration について、こうやるといいんじゃないかな的な話題をまとめていきます。(随時更新予定)
bigint な primary key
64bit にしたい場合。
exports.up = function(knex) { return knex.schema .createTable('xxxx', function (table) { table.specificType('id', knex.raw('BIGSERIAL PRIMARY KEY')); }); };enum の up / down
up / down の行き来によって enum 型定義だけ残るので強制的に消しに行ってます。
const drop_type_stmt = 'drop type if exists xxx_type'; exports.up = function(knex) { return knex.schema .raw(drop_type_stmt) .createTable('xxx', function (table) { table.enu('state', ['a', 'b'], { useNative: true, enumName: 'xxx_type' }); }); }; exports.down = function(knex) { return knex.schema .raw(drop_type_stmt); };
- 投稿日:2020-05-28T10:29:51+09:00
Javascript コーディング問題(手助け求めます)
Javascript のコーディング質問
Javascriptにて
あなたは現在価格mドルの土地の購入するために、年利i(0 < i < 100)%の金融商品にpドル投資しました。何年後に土地の購入ができるでしょうか?howLongToReachFundGoalという関数を再帰によって作成してください。なお、毎年得られた利益は同商品に再投資するとし、土地の価格は経過する年数が偶数(0を含む)の時は2%、奇数の時は3%上昇します。また、人の寿命は80歳未満と仮定し、80年以上かかる時は80としてください。
という内容の元プログラムを組んでいます。
問題に直面しているのですが、一定数の入力だと正解の出力より1多い数値を出してしまうのですが、改善方法がわからず行き詰まってしまっています。
(例)
入力:5421,10421,5
正解の出力:27
自分の出力:28どなたか分かる方手助けお願いします。
function howLongToReachFundGoalHelper(capitalMoney, goalMoney, interest, x){ function capitalAfterInterest(capitalMoney, interest, x){ if(x == 0){ return capitalMoney; } else { return capitalMoney * (1 + (interest / 100)); } } function goalMoneyAfterInflation(goalMoney, x){ if((x % 2 == 0) || (x == 0)){ return goalMoney * 1.02; } else { return goalMoney * 1.03; } } if(x >= 80){ return 80; } else if(capitalMoney >= goalMoney){ return x; } else { return howLongToReachFundGoalHelper(capitalAfterInterest(capitalMoney, interest, x + 1), goalMoneyAfterInflation(goalMoney, x + 1), interest, x + 1); } } function howLongToReachFundGoal(capitalMoney,goalMoney,interest){ //ここから書きましょう return howLongToReachFundGoalHelper(capitalMoney, goalMoney, interest, 0); }
- 投稿日:2020-05-28T10:16:09+09:00
React ContextでuseReducerを使ってみる
はじめに
今回はContext APIを用いた既存のプロジェクトにHooksの一つであるuseReducerを用いてreducerを追加していきます。ドキュメントを読んでもいまいち理解できない方向けの記事にしましたので細かい説明は省いています。そのままコピペしてみても良いかもしれません。
useReducerとは何か
(ContextAPIを用いて作られたグローバルなstateに対して)reducerを設定できるReact Hooksの一つです。つまりContextでreducerを使えます。ただ、そもそもreducerとは何か。
reducerとは何か
もともとはReduxにおいて出てくる考え方で、グローバルなstateの状態を管理する場所です。ローカルなstateの更新はstateを設定したコンポーネント内で出来ますが(下の図参照)、reduxで作成するグローバルなstateはコンポーネント側が直接変更することは出来ません。各コンポーネントはreducerに対して、『この処理を行いたい』という申請を送ると、reducerが代わりにstateの値を更新します。
App.jsimport React, { useState } from 'react'; const App () => { const = [data, setData] = useState('before'); const handleClick = () => { setData('after'); }; return ( <div className="app"> <div>{ data }</div> <button onClick={handleClick}>Change state</button> </div> ) }; export default App;useReducerの使い所
ContextAPIを用いて作られたグローバルなstateの状態を管理する場所としてreducerを設定します。reduxと違いContextはreducerを必要といないため、Context内でstateを更新する関数を定義しても良いのですが、大きなアプリケーションを作る際はstateとそれを更新する関数をreducerを使って分けた方が見やすいコードになり、何よりContextのコード量を減らせます。それでは実際にContextでreducerを設定しましょう。
Context × Reducer
useReducerを追加前のプロジェクト
まずはグローバルなstateであるContextを持つ簡単なReactプロジェクトを作成します。ファイル構成、コードは以下の通りです。以下のプロジェクトにuseReducerを用いてContextにreducerを追加します。
とりあえずはHooksのuseStateを用いてローカルなstateを作成し、Providerにstateを渡しグローバルなstateを作成します。
contexts/UserContext.jsimport React, { createContext, useState } from 'react'; const initialUsers = [ { name: 'Taro', age: 20, id: 1 }, { name: 'Kai', age: 18, id: 2 }, { name: 'Ryo', age: 23, id: 3 } ] export const UserContext = createContext(); const UserContextProvider = ({children}) => { const [users, setUsers] = useState(initialUsers); // もちろん下のようにstateの状態を管理する関数もreducerなしで作れます。 const deleteUser = (id) => { const newUsers = users.filter(user => user.id !== id) setUser([ ...newUsers ]) } return ( <UserContext.Provider value={{users, deleteUser}}> {children} </UserContext.Provider> ) } export default UserContextProvider;子コンポーネント(UserList)をProviderで囲んであげます。
src/App.jsimport React from 'react'; import UserContextProvider from './context/UserContext'; import UserList from './components/UserList'; const App = () => { return ( <UserContextProvider> <UserList /> </UserContextProvider> ); } export default App;stateの中身を簡単に表示させます。
components/UserList.jsimport React, { useContext, Fragment } from 'react'; import { UserContext } from '../context/UserContext'; const UserList = () => { const { users, deleteUser } = useContext(UserContext); const userList = users.map(user => { return ( <section key={user.id}> <h3>{ user.name }</h3> <div>年齢:{ user.age }歳</div> <button type="button" onClick={() => deleteUser(user.id)} >ユーザーを消去</button> </section> ) }) return ( <Fragment> <h2>ユーザーリスト</h2> {userList} </Fragment> ); } export default UserList結果として、ブラウザ上ではこのように表示されます。(黒の枠線はありません。)
useReducerを追加後のプロジェクト
それではuseReducerを用いてContextにreducerを追加します。useReducerを使う前に、reducerを作ります。まずはsrcの下にreducersフォルダを作成し、その中に実際のreducerとなるUserReducer.jsを作ります。dispatch関数を用いて送られてきたオブジェクトをもとにstateを操作します。返り値には更新後のstateを返しています。これだけでは掴みにくいと思うので下のコードで変更した部分を確認してみてください。
src/reducers/UserReducer.js// 下のuserReducerの引数actionは // dispatch関数を用いて送られてきたオブジェクトが入ります export const userReducer = (state, action) => { switch (action.type) { case 'DELETE_USER': // 今回はありませんがusersを含む全てのstateを返し // その後usersの中身を更新しています return { ...state, users: [ ...action.payload ] } default: return state } }contexts/UserContext.jsimport React, { createContext, useReducer } from 'react'; import { userReducer } from '../reducers/UserReducer'; // 変更部分。分かりやすくするためにオブジェクトとします。 const initialState = { users: [ { name: 'Taro', age: 20, id: 1 }, { name: 'Kai', age: 18, id: 2 }, { name: 'Ryo', age: 23, id: 3 } ] } export const UserContext = createContext(); const UserContextProvider = ({children}) => { // 変更部分。useReducerに使いたいreducer、stateの初期値を入れます。 // useStateと似ていてstateとdispatch関数を返します。 const [state, dispatch] = useReducer(userReducer, initialState); // 変更部分。stateとdispatch関数をグローバルで使えるようにします。 return ( <UserContext.Provider value={{state, dispatch}}> {children} </UserContext.Provider> ) } export default UserContextProvider;components/UserList.jsimport React, { useContext, Fragment } from 'react'; import { UserContext } from '../context/UserContext'; const UserList = () => { // 変更部分。 const { state: { users }, dispatch } = useContext(UserContext); // 変更部分。引数のidを用いて、そのidのユーザーを除いたnewUsersを作成。 // dispatch関数を呼び出し、typeを指定し、ペイロードとしてnewUsersを入れる。 const deleteUser = (id) => { const newUsers = users.filter(user => user.id !== id); dispatch({ type: 'DELETE_USER', payload: newUsers }) } const userList = users.map(user => { return ( <section key={user.id}> <h3>{ user.name }</h3> <div>年齢:{ user.age }歳</div> <button type="button" onClick={() => deleteUser(user.id)} >ユーザーを消去</button> </section> ) }) return ( <Fragment> <h2>ユーザーリスト</h2> {userList} </Fragment> ); } export default UserListご覧の通りuseReducer追加前のものと同じ機能を持つアプリができました。これで完成です!
最後に
ここまで読んでいただきありがとうございます。誤字、説明等の間違いがあった場合コメントください。
- 投稿日:2020-05-28T09:22:57+09:00
JavaScript基本文法(初学者向け)
コンソールにテキストを表示
console.log()
引数として()内にコンソールに表示したい情報を渡します。変数を渡すことも可能。変数の宣言
let name = 'sasaki'
const name = 'ken'
変数の宣言はlet
を使用。後で書き換え可能の宣言はlet
、
書き換え不能の宣言はconst
を使用します。条件分岐
if (条件式1) { // 条件式1がtrueのときの処理 } else if (条件式2) { // 条件式1がfalseで条件式2がtrueのときの処理 } else { // 条件式1も条件式2もfalseのときの処理 }条件式は()で括ること。
条件に対しての処理は{}で括ること。
複数の条件が必要の場合は、elseの後にifを書きます。配列
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS'];配列定義する場合は[]内に記述 。
要素番号は0からスタートで、この場合はRubyが0番目の要素です。この配列に対して以下の4つの操作を紹介▼
①配列の要素を取得する
▶︎配列の要素を取得するにはconsole.log(配列を代入した変数名[要素番号])
で取得。②配列の要素数を取得する
▶︎配列の要素数を取得するにはconsole.log(配列を代入した変数名.length)
で取得。③配列の要素を追加する
▶︎配列の要素を追加するには、push.配列を代入した変数名(追加したい要素)
で追加。④配列の要素を削除する
▶︎配列の要素を削除するには、配列を代入した変数名.pop()
で末尾の要素を削除。先頭の要素を削除する場合は、shiftメソッドを使用。なお、JavaScriptでは取り除く要素を指定できない模様。オブジェクト
オブジェクトを定義するには
let 変数名 = {オブジェクト名: 値}
で定義。
オブジェクトは名前と値にて情報を管理しており、それをプロパティと呼ぶ。オブジェクトに対して以下の2つの操作を紹介。
①プロパティの値を取得する
▶︎プロパティの値を取得するには、console.log(オブジェクトを代入した変数名.オブジェクト名)
で取得する。②プロパティの値を変更する
▶︎プロパティの値を変更するには、オブジェクトを代入した変数名.オブジェクト名 = 新たに変更したい値
で変更する。繰り返し処理
繰り返し処理
for文
を使用します。基本的な構文は下記の通りです。for (let i = 0; i < 繰り返す回数; i = i + 1) { // 繰り返す処理の内容 }
i = i + 1
の部分はiに1を追加した数値が表示されます。なおi = i + 1
の部分は繰り返す毎に呼ばれます。i += 1
に省略可能。関数宣言と関数式
#関数宣言 function 関数名(引数) { // 処理の内容 } #関数式(無名関数) let hello = function(){ console.log('hello'); }関数宣言と関数式の役割に大きな違いはありません。
ただし、関数式の方が何かに代入したり、他の関数に渡したりしやすい傾向にあります。
また、関数に引数が含まれない場合でも()は記述します。
- 投稿日:2020-05-28T09:17:58+09:00
GatsbyにTableOfContents(目次)をつける
はじめに
本記事は https://tech-blog.yoshikiohashi.dev/posts/start-gatsby-blog-tableofcontent のクロスポスト記事になります。
この記事はGatsbyというヘッドレスCMS技術で構成されています。今回は「エンジニア初心者でもできる」を前提に以下の構成で記事を作成していこうと思います。
- Gatsby始めるまで
- GatsbyにShare機能、OGPタグをつける
- タグ機能、カテゴリ機能をつける(基礎編)
- タグ機能、カテゴリ機能をつける(応用編)
- GatsbyにTableOfContents(目次)をつける(本記事)
- DarkModeをつける(別記事予定)
内容
前回はタグ一覧と記事一覧のコンポーネントを同時に出すGrapuQLクエリーの応用まで行いました。
今回はブログで欠かせないTableOfContents(目次)の実装方法のご紹介です。全く難しくないのでササッと行きましょう!
クエリー
超簡単です。記事を取得しているクエリーにtableOfContentsを付け足すだけです。エディタで結果を確認してみましょう。
query PostBySlug($slug: String!) { markdownRemark(fields: { slug: { eq: $slug } }) { id html fields { slug tagSlugs } frontmatter { date description tags title socialImage } tableOfContents } }このようにHTML形式のデータを取得することができました。あとは表示するだけです。
コンポーネント作成
TOCを表示するコンポーネントつくりです。
ちなみになぜコンポーネントにするのか?理由は単純で
- 使い回しがしやすい
- CSSの設定を限定的にできる
- パーツを好きなところに配置しやすくなる
からです。パーツ1つ1つの依存度を下げていきましょう。
下のようにdangerouslySetInnerHTML={{ __html: tableOfContents }}に先程取得したHTMLデータを流し込みましょう。(CSSの設定はお好みで設定してください)
const Toc = ({ tableOfContents, gridArea }: Props) => ( <div className={styles.toc} dangerouslySetInnerHTML={{ __html: tableOfContents }} /> ); export default Toc;まとめ
いかがだったでしょうか?
他にもgatsby-remark-tocなどのライブラリがあるみたいですが、私個人としてはこちらの方がシンプルで簡潔だと考えています。それでは次回の記事で。
- 投稿日:2020-05-28T09:16:15+09:00
Gatsbyにタグ機能、カテゴリ機能をつける(応用編)
はじめに
本記事は https://tech-blog.yoshikiohashi.dev/posts/start-gatsby-blog-add-tags-application のクロスポスト記事になります。
この記事はGatsbyというヘッドレスCMS技術で構成されています。今回は「エンジニア初心者でもできる」を前提に以下の構成で記事を作成していこうと思います。
- Gatsby始めるまで
- GatsbyにShare機能、OGPタグをつける
- タグ機能、カテゴリ機能をつける(基礎編)
- タグ機能、カテゴリ機能をつける(応用編)(本記事)
- GatsbyにTableOfContents(目次)をつける
- DarkModeをつける(別記事予定)
内容
前回はGraphQLを使用して、データを取得したJsonデータをTemplateに当てはめて画面に表示させるところまで行いました。 データの流れを理解できたでしょうか?この流れを理解するとあとは簡単です。
今回は応用編です。
「実際にはタグだけのページって必要でしょうか?」私は必要ないと思います。できれば、ユーザとしては選んだタグと記事の一覧が見れたら使いやすいはずです。
こんなタグ一覧+記事一覧ページを作るところをゴールに目指しましょう。
まずはタグ一覧+記事一覧のテンプレートファイルの作成
最初にGraphQLのクエリーを完成させよう
ポイントになってくるのがquery TagsListTemplate($tag: String!)とtags: { glob: $tag }のTemplateの引数の指定の仕方と使い方です。globはglobalの略でいわゆる何でも検索ができます。*を入れたらすべてのタグが見つかるようになります。
タグ一覧(/tags)では全タグを表示させたいので*が入ってきますね。
query TagsListTemplate($tag: String!) { allMarkdownRemark( filter: { frontmatter: { template: { eq: "post" }, draft: { ne: true }, tags: { glob: $tag } } }, sort: { order: DESC, fields: [frontmatter___date] } ){ group(field: frontmatter___tags) { fieldValue totalCount } edges { node { fields { slug categorySlug } frontmatter { title date category description socialImage } excerpt } } } }あとは、記事を取得するクエリーとタグを取得するクエリーを記述しましょう。
group(field: frontmatter___tags) { fieldValue totalCount } edges { node { fields { slug categorySlug } frontmatter { title date category description socialImage } excerpt } }GraphQLを実際に
http://localhost:8000/___graphql
で試してJsonが取得できたら次へ進みましょう。次は、取得したJsonをComponentに当てはめていきます。
const TagsListTemplate = ({ data, pageContext }) => { const tags = data.allMarkdownRemark.group; const posts = data.allMarkdownRemark.edges; return ( <div className={TagsList}> <Tags tags={tags} selectedTag={pageContext.tag}/> <Post posts={posts} /> // それぞれの記事を表示するコンポーネントに合わせて差し替えてください </div> ); }; export const query = graphql` query TagsListTemplate($tag: String!) { allMarkdownRemark( filter: { frontmatter: { template: { eq: "post" }, draft: { ne: true }, tags: { glob: $tag } } }, sort: { order: DESC, fields: [frontmatter___date] } ){ group(field: frontmatter___tags) { fieldValue totalCount } edges { node { fields { slug categorySlug } frontmatter { title date category description socialImage } excerpt } } } } `; export default TagsListTemplate;ここまででGraphQLクエリーでタグと記事の一覧を取得するクエリーを書き、Componentに当てはめることまでしました。次はタグのコンポーネントの作成です。
タグ一覧を表示するコンポーネントパーツの作成
先程作成したTemplateから呼び出されるタグコンポーネントです。こちらの例ではカウント順にソートしていますが、してもしなくてもOKです。
また、引数に
selectedTag
がありますが、選択している状態を表現したかったので表示しています。好みで変更してOKです。const sortTotalCount = (tags) => orderBy(tags, ['totalCount', 'fieldValue'], ['desc']); const Tags = ({ tags, selectedTag }: Props) => ( <div className={styles["Tags"]}> {sortTotalCount(tags).map(tag => ( <li key={tag.fieldValue}> <Link to={`/tags/${kebabCase(tag.fieldValue)}/`} className={selectedTag === tag.fieldValue ? styles['Tags--Selected'] : '' }> {tag.fieldValue} <span>{tag.totalCount}</span> </Link> </li> ))} </div> ); export default Tags;gatsby-node.js に /tags/ と /tags/{tags} のルーティングを作成する
gatsby-node.js
まずはタグ一覧ページを登録します。
すべてのタグを表示するのでcontextに{ tag: "*" }を指定しています。
// Tags list createPage({ path: '/tags', component: path.resolve('./src/templates/tags-list-template.js'), context: { tag: "*" } });タグが選ばれた場合のURLを登録していきます。
ポイントはcontextの{ tag: tag.fieldValue }を指定している箇所です。これでURLが/tags/{tags}だったときにタグ名がTemplateに引数として受け渡しされることになります。
const result = await graphql(` { allMarkdownRemark( filter: { frontmatter: { template: { eq: "post" }, draft: { ne: true } } } ) { group(field: frontmatter___tags) { fieldValue totalCount } } } `); _.each(result.data.allMarkdownRemark.group, (tag) => { const numPages = Math.ceil(tag.totalCount / postsPerPage); const tagSlug = `/tags/${_.kebabCase(tag.fieldValue)}`; for (let i = 0; i < numPages; i += 1) { createPage({ path: i === 0 ? tagSlug : `${tagSlug}/page/${i}`, component: path.resolve('./src/templates/tags-list-template.js'), context: { tag: tag.fieldValue } }); } }処理の流れを表すとこのような図になります。
gatsby build を再度実行
/tags のページに遷移してみましょう。タグ一覧とその記事が表示されましたか?
まとめ
いかがだったでしょうか?うまく設定できたでしょうか?ここまで理解できたら他の実装にも応用できますね。それでは次回の記事で。
- 投稿日:2020-05-28T09:14:27+09:00
Gatsbyにタグ機能、カテゴリ機能をつける(基礎編)
はじめに
本記事は https://tech-blog.yoshikiohashi.dev/posts/start-gatsby-blog-add-tags のクロスポスト記事になります。
この記事はGatsbyというヘッドレスCMS技術で構成されています。今回は「エンジニア初心者でもできる」を前提に以下の構成で記事を作成していこうと思います。
- Gatsby始めるまで
- GatsbyにShare機能、OGPタグをつける
- タグ機能、カテゴリ機能をつける(基礎編)(本記事)
- タグ機能、カテゴリ機能をつける(応用編)
- GatsbyにTableOfContents(目次)をつける
- DarkModeをつける(別記事予定)
内容
今回はWordPressのタグ機能、カテゴリ機能に当たる部分を実装していきます。標準で実装されてないの?!って突っ込みはあるかと思いますが、そうです。標準のテンプレートでは実装されておりません。なので実装するよりも最初からあるテンプレートを選んだほうが良いです。
実装する前に
https://www.gatsbyjs.org/docs/adding-tags-and-categories-to-blog-posts/
まずこちらの記事を読みましょう。GatsbyJS本家でタグの付け方について記載されています。
手順
- markdownファイルにタグを追加する
- GraphQLクエリを作成し、全てのタグを取得する
- タグページテンプレートを作成する(/tag/{tag})
- 作成したテンプレートを使用して、gatsby-node.jsでページをレンダリングする
- すべてのタグのリストを表示するタグインデックスページを作成する(/tags)
1.markdownファイルにタグを追加する
タグがない場合のマークダウンファイル。ない場合追加する必要があります。
--- title: "A Trip To the Zoo" --- I went to the zoo today. It was terrible.tagsという名前で項目を追加しました。タグは複数個設定される想定なので、配列として定義します。他にも、文字列、数値が設定できます。(カテゴリも一緒で単一のため文字列として設定します。)
--- title: "A Trip To the Zoo" tags: ["animals", "Chicago", "zoos"] --- I went to the zoo today. It was terrible.ローカル環境でgatsby developが実行されている場合は、再起動すると、Gatsbyが新しい項目を取得できるようになります。
2. GraphQLクエリを作成し、全てのタグを取得する
GraphQLを確認するためにはローカルでgatsby developを実行し、http://localhost:8000/___graphqlにアクセスします。
下のように画面が見えるはずです。
画面が表示されたら下のクエリーを入力してみましょう。
{ allMarkdownRemark { group(field: frontmatter___tags) { tag: fieldValue totalCount } } }タグ一覧が取得できるはずです。
ちなみにgroup()はSQLのGroupByと同じような意味合いです。ここではタグ項目でグルーピングしています。
3. タグページテンプレートを作成する(/tag/{tag})
ディレクトリはsrc/template/tags.jsなどの配置にしましょう。
import React from "react" import PropTypes from "prop-types" // Components import { Link, graphql } from "gatsby" const Tags = ({ pageContext, data }) => { const { tag } = pageContext const { edges, totalCount } = data.allMarkdownRemark const tagHeader = `${totalCount} post${ totalCount === 1 ? "" : "s" } tagged with "${tag}"` return ( <div> <h1>{tagHeader}</h1> <ul> {edges.map(({ node }) => { const { slug } = node.fields const { title } = node.frontmatter return ( <li key={slug}> <Link to={slug}>{title}</Link> </li> ) })} </ul> {/* This links to a page that does not yet exist. You'll come back to it! */} <Link to="/tags">All tags</Link> </div> ) } Tags.propTypes = { pageContext: PropTypes.shape({ tag: PropTypes.string.isRequired, }), data: PropTypes.shape({ allMarkdownRemark: PropTypes.shape({ totalCount: PropTypes.number.isRequired, edges: PropTypes.arrayOf( PropTypes.shape({ node: PropTypes.shape({ frontmatter: PropTypes.shape({ title: PropTypes.string.isRequired, }), fields: PropTypes.shape({ slug: PropTypes.string.isRequired, }), }), }).isRequired ), }), }), } export default Tags export const pageQuery = graphql` query($tag: String) { allMarkdownRemark( limit: 2000 sort: { fields: [frontmatter___date], order: DESC } filter: { frontmatter: { tags: { in: [$tag] } } } ) { totalCount edges { node { fields { slug } frontmatter { title } } } } } `補足
基本的にsrc/template/の配下のJSファイルはGraphQLがセットになっている構成が望ましいです。(必要でなければなくても良いです。)
流れで表すと下のようなイメージです。
GraphQL == クエリ結果 => Template == クエリ結果 => Componentの各パーツ
4. 作成したテンプレートを使用して、gatsby-node.jsでページをレンダリングする
さて、テンプレートページは完成したのであとはGatsby buildをするときにタグページを読み込ませるだけです。Gatsbyは最初に指定URLのページを読み込ませてビルドすることで静的なページが作成されていきます。
ここのgatsby-node.jsでsrc/templates/tags.jsに対してGraphQLで取得した結果をfor文でタグ数分生成しています。
const path = require("path") const _ = require("lodash") exports.createPages = async ({ actions, graphql, reporter }) => { const { createPage } = actions const blogPostTemplate = path.resolve("src/templates/blog.js") const tagTemplate = path.resolve("src/templates/tags.js") const result = await graphql(` { postsRemark: allMarkdownRemark( sort: { order: DESC, fields: [frontmatter___date] } limit: 2000 ) { edges { node { fields { slug } frontmatter { tags } } } } tagsGroup: allMarkdownRemark(limit: 2000) { group(field: frontmatter___tags) { fieldValue } } } `) // handle errors if (result.errors) { reporter.panicOnBuild(`Error while running GraphQL query.`) return } const posts = result.data.postsRemark.edges // Create post detail pages posts.forEach(({ node }) => { createPage({ path: node.fields.slug, component: blogPostTemplate, }) }) // Extract tag data from query const tags = result.data.tagsGroup.group // Make tag pages tags.forEach(tag => { createPage({ path: `/tags/${_.kebabCase(tag.fieldValue)}/`, component: tagTemplate, context: { tag: tag.fieldValue, }, }) }) }5. すべてのタグのリストを表示するタグインデックスページを作成する(/tags)
今度はタグ一覧ページを作成していきます。前に書いたGraphQLクエリーでタグ一覧を取得し、Templateに取得結果を当てはめていきます。
import React from "react" import PropTypes from "prop-types" // Utilities import kebabCase from "lodash/kebabCase" // Components import { Helmet } from "react-helmet" import { Link, graphql } from "gatsby" const TagsPage = ({ data: { allMarkdownRemark: { group }, site: { siteMetadata: { title }, }, }, }) => ( <div> <Helmet title={title} /> <div> <h1>Tags</h1> <ul> {group.map(tag => ( <li key={tag.fieldValue}> <Link to={`/tags/${kebabCase(tag.fieldValue)}/`}> {tag.fieldValue} ({tag.totalCount}) </Link> </li> ))} </ul> </div> </div> ) TagsPage.propTypes = { data: PropTypes.shape({ allMarkdownRemark: PropTypes.shape({ group: PropTypes.arrayOf( PropTypes.shape({ fieldValue: PropTypes.string.isRequired, totalCount: PropTypes.number.isRequired, }).isRequired ), }), site: PropTypes.shape({ siteMetadata: PropTypes.shape({ title: PropTypes.string.isRequired, }), }), }), } export default TagsPage export const pageQuery = graphql` query { site { siteMetadata { title } } allMarkdownRemark(limit: 2000) { group(field: frontmatter___tags) { fieldValue totalCount } } } `カテゴリーの実装
カテゴリー機能も考え方はタグと一緒です。タグは1つの記事に対し複数個の配列で構成され、カテゴリーは1記事に対して、単一の項目なので文字列としてマークダウンファイルに記述する必要があります。
実装手順
- markdownファイルに文字列型のカテゴリーを追加する
- GraphQLクエリを作成し、全てのカテゴリーを取得する
- カテゴリーページテンプレートを作成する
- 作成したテンプレートを使用して、gatsby-node.jsでページをレンダリングする
- すべてのカテゴリーのリストを表示するインデックスページを作成する
実装内容はほぼ一緒なので割愛します
まとめ
いかがだったでしょうか?ちょっと今回は難しかったでしょうか?このブログのソースコードは公開されているので良ければ参考にどうぞ。それでは次回の記事で。
https://github.com/yoshiki-0428/engneer-blog/blob/master/src/templates/tags-list-template.js#L29-L53
- 投稿日:2020-05-28T09:12:27+09:00
GatsbyにShare機能、OGPタグをつける
はじめに
本記事は https://tech-blog.yoshikiohashi.dev/posts/start-gatsby-blog-share/ のクロスポスト記事になります。
この記事はGatsbyというヘッドレスCMS技術で構成されています。今回は「エンジニア初心者でもできる」を前提に以下の構成で記事を作成していこうと思います。
- Gatsby始めるまで
- GatsbyにShare機能、OGPタグをつける(本記事)
- タグ機能、カテゴリ機能をつける(基礎編)
- タグ機能、カテゴリ機能をつける(応用編)
- GatsbyにTableOfContents(目次)をつける
- DarkModeをつける(別記事予定)
内容
今回はWordPressのシェアボタンなんかでよくあるSNSへのシェアボタンとOGP設定タグの付け方を解説していきます。
SNSへのシェアボタン
OGP設定タグ
SNSシェアボタンを実装する
react-shareのインストール
めっちゃ簡単です。まずライブラリをインストール。
yarn add react-share
ShareSns.jsのコンポーネントの作成
適当なディレクトリを作成して(ここではShareSnsとします)
export const ShareSns = ({ articleUrl, articleTitle }) => ( <div className={'ShareSns'}> <div> <FacebookShareButton url={articleUrl}> <FacebookIcon size={32} round /> </FacebookShareButton> <LineShareButton url={articleUrl}> <LineIcon size={32} round /> </LineShareButton> <LinkedinShareButton url={articleUrl}> <LinkedinIcon title={articleTitle} size={32} round /> </LinkedinShareButton> <TwitterShareButton title={articleTitle} via="yoshiki__0428" url={articleUrl}> <TwitterIcon size={32} round /> </TwitterShareButton> </div> </div> );呼び出し方
ちょっと呼び出し方がもどかしいですが、Gatsby buildをするときにwindow.location.hrefが未定義なのでビルド時に落ちてしまいます。そのため、typeofで確認する必要があります。
{typeof window !== 'undefined' && window.location.href && <ShareSns articleUrl={window.location.href} articleTitle={title} /> }もしくは呼び出し前にundifinedチェックをして値が存在するか事前に確認しても良いと思います。
const windowUrl = (typeof window !== 'undefined' && window.location.href) ? window.location.href : '';これで下のようなボタンが表示されるはずです。他にも色々なSNSのボタンがあるので使ってみてはいかがでしょうか。
OGP設定タグ
react-helmetのインストール
大抵のTemplateには入っているのでやらなくても良いかもです。
yarn add react-helmet
Metaタグ専用のコンポーネントの作成
使い方は簡単です。Helmetタグを作成し、そこに必要なMetaタグを入力すればOKです。
この例ではOGとTwitterのOGPを設定しています。呼び出し元は画像データやタイトルデータが取得できる記事画面で呼び出すと良いと思います。Templateによって設計がまちまちなので環境に合わせて適用してみてください。
<Helmet> <html lang="jp" /> <title>{title}</title> <meta name="description" content={description} /> <meta property="og:site_name" content={title} /> <meta property="og:image" content={image} /> <meta name="twitter:card" content="summary" /> <meta name="twitter:title" content={title} /> <meta name="twitter:description" content={description} /> <meta name="twitter:image" content={image} /> </Helmet>OGPタグの設定確認
netlifyにアップロードしたらTwitterのカード情報が確認できるサイトで対象のURLを入力して確認してみましょう。設定できていればOKです。Slackのチャットなんかにも投稿しても確認できます。
まとめ
いかがだったでしょうか?結構簡単に設定できたんじゃないかなと思います。ここらへんの機能もブログをやるのであれば必須なので是非やっておきましょう。それでは次回の記事で。
- 投稿日:2020-05-28T06:53:09+09:00
Puppeteerを2系から3系にアップデートしたらError: Failed to launch the browser process!
Puppeteerメモです。
Heroku上で利用していたPuppetterのバージョンを2系->3系にアップデートしたらエラー発生。
Puppetterのバージョンを2系->3系にするとChromiumのバージョン違いで動かないっぽい
Heroku上のログ
Error: Failed to launch the browser process! 2020-05-27T21:22:10.217234+00:00 app[web.1]: /app/node_modules/puppeteer/.local-chromium/linux-756035/chrome-linux/chrome: error while loading shared libraries: libgbm.so.1: cannot open shared object file: No such file or directoryHeroku上でインストールされているChromiumを更新する
Puppetterのv3.1.0ではChromium 83.0.4103.0 (r756035)を利用していると、Big changesと書かれていて、内部で利用するChromiumのバージョンが違う模様です。
最初にインストールしたときはPuppetterのv2系だったので、その時に対応していたバージョンのChromiumがHeroku側に残ったままになってるんだと思います。
bildpackを再ビルド
$ heroku buildpacks:add jontewks/puppeteerその後デプロイしなおす(
git push heroku master
)と再度ビルドされてHeroku上のChromiumも更新されてこれで問題なく利用できるようになりました。その他メモ
その他触っててでたエラーをメモ的に残しておきます。
TimeoutError: Navigation timeout of 30000 ms exceeded
タイムアウト。
デフォルトだと30秒待ってくれるみたいだけど、それ以上かかる場合もありそう。
page.goto()
のオプションで{waitUntil: 'networkidle'}
を指定するっぽいけど更にエラーがError: ERROR: "networkidle" option is no longer supported. Use "networkidle2" insteadなるほど
page.goto(url, {waitUntil: 'networkidle2'})でうまくいった。
Error: Evaluation failed: TypeError: Cannot set property 'value' of null
こんな雰囲気でiPhoneでのページ閲覧エミュレートをしてましたが、
const devices = require('puppeteer/DeviceDescriptors'); const iPhone = devices['iPhone X']; 省略 await page.emulate(iPhone);これもv2->3にあげたら
Error: Evaluation failed: TypeError: Cannot set property 'value' of null
とエラーが発生し、うまく動作しなくなってしまいました。これパッと調べても原因や解決策がわからなかったので、iPhoneでのエミュレートを泣く泣く断念。
今回やりたかったのはデスクトップエミュレートでもなんとかなったのでよかったけど、必要になったらまた調べないと...
- 投稿日:2020-05-28T04:04:42+09:00
Firebaseの技術を使ってWebページをもう少し進化させる
今回、FirebaseのRealtime DatabaseとCloud Storageの仕組みを使って今までのwebページの機能を増やしました。
Firebaseの全体図と各機能の理解については
以下が非常に参考になりました。
https://www.topgate.co.jp/firebase01-what-is-firebaseFirebaseとは
Firebase は Google が提供しているモバイルおよび Web アプリケーションのバックエンドサービスです。クラウドサービスの形態では BaaS に位置付けされます。 Firebase を使うことで、バックエンドで動くサービスを作成する必要も管理する必要がなくなり、開発者はアプリケーションの開発に専念できます。
Realtime Databaseとは
Firebase に元から含まれているオブジェクト型のデータベースです。リアルタイムでクライアント全体の状態を同期させることができ、オフラインで動作するときはデータをキャッシュしてオンラインになった時に自動的にデータを同期します。
Cloud Storage for Firebaseとは
写真や動画などバイナリーデータを保存します。保存先は Cloud Storage となっており、 Firebase と Google Cloud の両方からアクセスできます。また、スケールアウト機能も兼ね備えており、急激なアプリケーションの拡大にも対応しています。
公開Webページ
今回追加した機能
②画像投稿ページ
コード
HTML
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Voxel Ant | あなただけのブロックアートを。</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"> <link rel="stylesheet" href="main.css"> </head> <header> <link rel="stylesheet" href="main.css"> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <h1> <a href="index.html"><img src="ant-logo4.png" alt="街を作ろう"></a> </h1> </header> <body> <section id="loading" class="view"> <i class="initial-loading-icon fas fa-sync" aria-hidden="true"></i> </section> <div id="logo-gazo"> <img src="ant-logo3.png"> </div> <!-- /#ログイン画面 --> <section id="login" class="view"> <div class="container"> <div class="row justify-content-center"> <div class="col-sm-10 col-md-8 col-lg-6"> <form id="login-form"> <div class="form-group login__email"> <label for="login-email" class="col-form-label"> メールアドレス </label> <div> <input id="login-email" type="email" class="form-control" required> </div> </div> <div class="form-group login__password"> <label for="login-password" class="col-form-label"> パスワード </label> <div> <input id="login-password" type="password" class="form-control" required> </div> </div> <div id="login__help" class="alert alert-danger"></div> <div class="form-group login__submit"> <div> <button id="login__submit-button" type="submit" class="btn btn-success"> ログイン </button> </div> </div> </form> </div> </div> </div> </section> <!-- /#ログイン画面 --> <!-- 一覧画面 --> <section id="blcokgallery" class="view"> <header> <div id="header"> <a href="#add-block-modal" data-toggle="modal" class="add-button"> <i class="fas fa-plus-circle" aria-hidden="true"></i> ブロックアートの登録 </a> <button class="btn btn-primary logout-button"> ログアウト </button> </div> </header> <div id="cover"> <h1 id="cover__title">ブロックアートギャラリー</h1> </div> <div class="wrapper"> <div id="main"> <div id="block-list" class="clearfix"></div> </div> </div> </section> <div id="block-template"> <div class="block-item"> <div class="block-item__image-wrapper"> <img class="block-item__image" alt=""> </div> <div class="block-item__detail"> <div class="block-item__title"></div> <div class="block-item__delete-wrapper"> <button class="btn btn-danger block-item__delete"> <i class="fas fa-trash-alt" aria-hidden="true"></i> </button> <div id="good"> <button-preference></button-preference> </div> <script src="sub1.js"></script> </div> </div> </div> </div> <div id="add-block-modal" class="modal fade" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">ブロックアートの登録</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body m-1"> <form id="block-form"> <div class="form-group row"> <label for="add-block-title" class="col-md-3 col-form-label">タイトル</label> <div class="col-md-9"> <input id="add-block-title" type="text" class="form-control" required> </div> </div> <div class="form-group row"> <div class="col-md-3">アート画像</div> <div class="col-md-9"> <div class="custom-file"> <input id="add-block-image" type="file" accept=".jpg,.jpeg,.png,.gif, image/jpeg,image/png,image/gif" class="custom-file-input" required> <label id="add-block-image-label" class="custom-file-label" for="add-block-image">ファイルを選択</label> </div> </div> </div> <div id="add-block__help" class="alert alert-danger"></div> <button id="submit_add_block" type="submit" class="btn btn-default btn-success btn-block"> 保存する </button> </div> </form> </div> </div> </div> <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-app.js"></script> <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-auth.js"></script> <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-database.js"></script> <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-storage.js"></script> <script> const firebaseConfig = { apiKey: "HOGEHOGE", authDomain: "HOGEHOGE", databaseURL: "HOGEHOGE", projectId: "HOGEHOGE", storageBucket: "HOGEHOGE", messagingSenderId: "HOGEHOGE", appId: "HOGEHOGE", measurementId: "HOGEHOGE" }; firebase.initializeApp(firebaseConfig); </script> <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script> <script src="main.js"></script> </body> </html>補足
以下については自身のAPIキー等に置き換えてください。
<script> const firebaseConfig = { apiKey: "HOGEHOGE", authDomain: "HOGEHOGE", databaseURL: "HOGEHOGE", projectId: "HOGEHOGE", storageBucket: "HOGEHOGE", messagingSenderId: "HOGEHOGE", appId: "HOGEHOGE", measurementId: "HOGEHOGE" }; firebase.initializeApp(firebaseConfig);参考にした記事・リンク
<技術編>
- https://www.topgate.co.jp/firebase01-what-is-firebase
- https://firebase.google.com/docs/database/web/read-and-write?hl=ja
- https://firebase.google.com/docs/database/web/save-data?hl=ja<独自ドメインでの公開>
- 爆速!Vercelとfreenomで独自ドメインのサイトを無料で作成する
https://qiita.com/n0bisuke/items/901154531ea14f978bd4終わりに
- 今回、時間がなく出来ませんでしたが、本当はAI技術を組み合わせた実装にも挑戦してみたかったです。
- 投稿日:2020-05-28T03:45:21+09:00
昔、WEBのカラーコードを調べる時によくこういうモノで調べてました。
体内リズムが崩れてしまった、taokaです(´・ω・`)。
昔、WEBのカラーコードを調べる時によくこういうモノで調べてました。
今ではプラグインやデベロッパーツールで簡単にカラーコードが分かってしまいます。※履歴を溜めてごめんなさい
https://taoka-toshiaki.com/qiita-no5/
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta name="Description" content="Enter your description here" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css"> <title>color/#Hexadecimal</title> <style> body { background-color: rgb(4, 6, 38); color: rgb(109, 135, 145); } h1{ background-color: aliceblue; } </style> </head> <body onload="crgb(1)"> <h1 class="h1 mt-5 mb-5"><a href="../">home</a></h1> <div class="form-group"> <label for="myinput1"> <h1 class="h1" style="color:red">赤</h1> </label> <input id="myinput1" class="form-control-range" type="range" name="" onchange="crgb()" value="4" min="0" max="255" step="1"> </div> <div class="form-group"> <label for="myinput2"> <h1 class="h1" style="color: green;">緑</h1> </label> <input id="myinput2" class="form-control-range" type="range" onchange="crgb()" name="" value="6" min="0" max="255" step="1"> </div> <div class="form-group"> <label for="myinput3"> <h1 class="h1" style="color: blue;">青</h1> </label> <input id="myinput3" class="form-control-range" type="range" onchange="crgb()" name="" value="38" min="0" max="255" step="1"> </div> <h1 class="h1" id="view"> </h1> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.slim.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.1/umd/popper.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/js/bootstrap.min.js"></script> <script> function crgb(off){ // http://www-creators.com/archives/4463 let getParam = function (name, url) { if (!url) url = window.location.href; name = name.replace(/[\[\]]/g, "\\$&"); var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url); if (!results) return false; if (!results[2]) return false; return parseInt(decodeURIComponent(results[2].replace(/\+/g, " "))); }; // https://taoka-toshiaki.com if(off){ document.getElementById("myinput1").value = getParam("r")!==false?getParam("r"):document.getElementById("myinput1").value; document.getElementById("myinput2").value = getParam("g")!==false?getParam("g"):document.getElementById("myinput2").value; document.getElementById("myinput3").value = getParam("b")!==false?getParam("b"):document.getElementById("myinput3").value; } document.body.style.backgroundColor = "rgb(" + document.getElementById("myinput1").value + "," + document.getElementById("myinput2").value + "," + document.getElementById("myinput3").value + ")"; // https://lab.syncer.jp/Web/JavaScript/Snippet/60/ let rgb2hex = function (rgb) { return "background-color:#" + rgb.map(function (value) { return ("0" + parseInt(value).toString(16)).slice(-2); }).join(""); }; // https://taoka-toshiaki.com document.getElementById("view").innerHTML = rgb2hex([document.getElementById("myinput1").value, document.getElementById("myinput2").value, document.getElementById("myinput3").value]); history.pushState(null, null, "?r=" + document.getElementById("myinput1").value + "&g=" + document.getElementById("myinput2").value + "&b=" + document.getElementById("myinput3").value); } </script> </body> </html>