- 投稿日:2020-03-14T23:12:16+09:00
トップページに戻るボタンをjavascriptで(jQuery無しで)
はじめに
脱jQueryしていきたいのでだんだんと素のjavascriptで書いていくようにしていると段階です。
備忘録がわりです。復習を兼ねて1つ1つ解説入れていこうと思います。対象
駆け出しエンジニアの方や学生,jQueryに頼りっぱなしが嫌な方。
ソースコード
main.js// トップへ戻るボタン function scrollTop(el,duration) { let target = document.getElementById(el); // elと名付けたドキュメント要素を取得し、変数に格納する target.addEventListener('click', function() { let currentY = window.pageYOffset;// クリック時の縦方向へのスクロール量を取得 let step = duration/currentY > 1 ? 10 : 100; // 三項演算子で10or100を変数step(1回分のスクロール量)に格納 let timeStep = duration/currentY * step; // 1回のスクロールにかかった時間を格納 let intervalID = setInterval(scrollUp, timeStep); // scrollUp()を一定時間ごとに繰り返す // scrollUp()で一定時間ごとに繰り返される関数 function scrollUp(){ currentY = window.pageYOffset; if(currentY === 0) { // 垂直方向へのスクロール量が0になったら clearInterval(intervalID); // 処理を停止する } else { scrollBy( 0, -step ); // スクロールした分だけY軸方向へ戻る } } }); } scrollTop('top-button',ページトップに到達する時間); //関数を呼び出すindex.html<div id="top-button"> <div class="arrow">divでボタンを作る</div> </div>解説
'top-button'とidで指定されたHTMLドキュメントをクリックしたら、クリックした時点でのスクロール量を取得し、そのスクロール量に応じて速度を場合分けして、ページのトップまでY軸方向へ戻るという処理。
setInterval(),clearInterval()
ここうまく説明できないけど、setInterval()をclearInterval()で包んで、ページのトップまでいったら処理を止めるイメージ
let step = duration/currentY > 1 ? 10 : 100;
三項演算子
if (条件式) { //Trueの処理 } else { //Falseの処理 }条件式 ? Trueの処理 : Falseの処理上の2つは同じ意味。複雑になると推奨されないらしいです。
今回ではduration/currentYの値が1より大きかったら10,1以下だったら100を変数stepに格納するという処理ですね。おわりに
勉強になりました。より良くなると教えてくれる方ぜひ教えてください。
- 投稿日:2020-03-14T23:01:57+09:00
Vue.jsプロジェクトのセットアップ
Vue.jsのプロジェクトを最初から作る手順を紹介します。
前提条件
- npm、yarnがインストール済みであること
@vue/cliのインストール
グローバルに@vue/cliをインストールしてvueコマンドを使用できるようにします。
$ npm install -g @vue/clivueプロジェクトの作成
$ vue create project-name
? Please pick a preset: (Use arrow keys) ❯ default (babel, eslint) Manually select features開発環境の起動
$ yarn serve
起動後、ブラウザからhttp://localhost:8080/で表示できます。
production用build
$ yarn build
成功すると、distディレクトリにファイルが作られます。
- 投稿日:2020-03-14T22:59:12+09:00
GTMをタグに戻す
GTMと聞いて別のサービスを思い浮かべるのは私だけでないはず。
そこで正しいサービスに戻してみようと思います。ここの記事を参考にさせていただきました。
LGTMを戻すgtm/gtm.jswindow.onload = function() { var img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAC0CAMAAAB4+cOfAAAApVBMVEX///9FhPA5abtYkPE9gPA1Z7pGcr9liMgwZLkoYLiOr/W+y+WIoNE1fO/R4PtBgvAweu+nwPfy9v73+v7s8v1glPLg6fzl7f1+pvQ+bb5Jhe3M2/q7z/l1ofPF1vra5fyYt/a90fmUtfavx/ijv/dqmvJ9pfTS2+5dkvJRi/GOsfWUq9mqvN+Fq/Szyfgmdu98mM9ZfsNSesLD0OihtNvX3/Czw+K2RX0AAAALAElEQVR4nO2dbUPiOBDHWxtWcDekPJQi8qQ8rKB4rnvn9/9oh+hqUpJ2JpmUlrt5bWPzI00yk/9MgsCbDabL7W6SRLWwZDIfp/5YfFq6WDHOGWMiFLWwUDDGk7FnLO3VnklYPxN8dOsTS8LrSOXd4knXE5b+motT987FWOhnqhnX8huSTQgPZHqruNbD5WAi7FFzSUXdh8vB2ISYy5SfuktExmnXpk69Z13ZRpRclucyXvbGn/7nojURkXGZnxOX/ZChWrKvzotLyIi8puGZcQnZHQmXyVlsX2QTJFuZ1dlxIZl9e8n5caHYyXSjc+QShq5c+puz2e6q5solJOIihGAlmQC9shuXASPhIljMVsOrkmwd8bj4vZ24tEm2LyJOxn23Hwhr6SIq8nhdmr+h4CL4cEDVX4y1V7EvMLcUXNimTdZX7PuHeeupfbtjivAL39F1FG29vB27dauL/JEI5ELjkljb0EzGtsklBRd2yvFyMLM3Y9ngHQUXkZB20sZ6xv2pXXs7kvWI+Tr0Q1hqGjJWra1J9i+8Q9xJK3swDBmbtojCLyPycy0b6zZHRGB6ROEXtqTvpY39aGjJoNvpUYUZyOLNjvazda0jg22mSxVmEPc+emljrYaODLKR/ogqLEUUbiawy+aFhgyujQEslgEC41vWBbaH5oWGDKqJGU345WD8xlM/0fbYutCQwbTQpozucjKvOu38MbtB+PoG5ogMogFalQcZmJR/Bi35yqaB7wcwWTLw529pxVJkYJbSOLby1j/AZMiAH3+icBt9gBnLL8an+Ab+gFHJQJ/uEHMhnGMiaSTbOKafYBQywIfn1FwoJ1/53QR+mvkCI5OBPbulVzPQgQkW8tvhxVASGIkM6FEfKg9CMMrHFHLsxySD+SIDedKLyoMSTCr/cGyIfFoB80kG8KAflQclmGAuv2KMbFkF84dM4WO+VB6kYAJ5ocX67RkwH2SKniILv/gFoxz+xTj9chbMO5mCh7ojXyoPWjBBIr8nTvVzBOZAJv+ZlErl4R1M237JPgbzRib3EcLwi28w6tK5wTypAXNxndvCzKdIlRrMTHGZMAEIHZjGt5wHSFQelGBmy+FqtTNpaVayy4QRXWLBeE6yQYMZJDF706NxvtUeSCm/IyY+iAQz9pyrhgWz+HofFmoHzUZqnSF8SRwYEpUHIRgl6qJPznuSp1/EqRUKjP9kEhyYvvo7sbXujxSPae4FzJ3/pAkcmHXmu451ur0racgI4QMMicqDEkw/+0Jsq/krZXsBdyXhYEpJskGBOdZBajdgsiupRecGppwkGxSYu6NXinXT79zqW4KCKSnJBgVmd/ROXDfJKN8Sn5GC6d2XlEziCkb7uPItzSnBkKk8aMHMjz8l7T5lK39LULcAAqafK5Y+HZijyVcw7d8pbkEMTFAAgEnhYQax91n43nGJuaUCArdcZ/fhhpzFrtwBaCGCYjAzMBfGkvntoLu3fvtpuLEppIPb4E2yGzyDkygH8tgVEZgZsHuCbzrqKG3v8FVjcGAGGZfAJJ1WFmxgULwIzADYNx5pDs77c+yoQTqRqkSfm6YPJVYCPMcuAGOUSKsm4oW++XSF2y9jww6SSJ8xo+us+A7Af5EPpgcLe7PI7M53UBEcdKBqGvJD8TgWD3OU00pQxvAbZiwfzD2oUzw3gR0lRbMIbU5395tk0sldhWV/BpjckgvmeGup7UzBfxogvibqYPiHWcy+eWBuQfG64tpNiOxRT2DG8i8My1bIAdOHbdIA/2cBJuMJjHrwBtr75oBZgbjEEHcVHLPwBEbZI4PeOAfMGPQzwzTuXaiH4AlMoETEQU6BEUwP1BWD23Zk0I/JF5hI/i1BqWJGMEvQ6AduCtSYyAnAyNMCbJCbwPRA/YAOmP0+DzbL+AIjnycIkOrMBOYJNsMAXVVNRL9cMEqsCnQeaQKTwPa88H7A1jhfYOboIJ4BDOwHxlQshc1ZvsB00DoZAxhY6QqM4Bq2/fUFZqFsfSFPGMAcx5l1hknU64L8C19g5E0ZbJwbwMBmBIZRtYEipL7ATKnAROZ3l7uBUYiCmiwFjIDE8AxgYPsxVAYjaBD6AnNTMhhMglRlwIQC4l4bwNzDwGA+JdDOqPKfUvbERm+oyXdT3F5Zk68DGPrlOjjpck22KsFUq5gN3uCkYGSJogClY5XmEiyq4xK47HzpnUhYdNMXmCWVr0QeduhWKOwAqg5WVqAK9iWVE6gCFbguK7QJWqxLCm2ClJtGMF2Q0wdVQcK+TH9glMNrt2A4dJaBHZ9AxUeewPSU4xNQ2lLOgRssURZ0fLWu0oEb7H/kgElBhVbFpngvA/2QanJECz2LLNz+IpLiPIFRN76uh/q59VtlMtpsmC9rI0r8egKD38YUCIdgeef5Y+YGU/oYD6Y3XkfhJtnmPqhkRhIIh6CrCYvMny3uRi40mFvG328AjVdmuVtPTqAnkZpBRZtmcWLiV5y4k3Iizetjil+U6OSs9zo5651nOauScyf0mQRBRlsPrCRDKYBeZATQQ98C6BlQAK1k/xEJoLGS+em7ZP7mTTJvkU6AA5NNojJJ5iO0p1TvJIs0GxQ0KFX7ikMAPNmoc1rO09Fr6WNzNlNMrRO5tkdgYm2vh/IuBlrD/rxS//QrtvwX4NLbZ5YsqgOjJrhB75qpc3rxcZBRm9d3hT4hQIGpYEK65vBL92fyogov7lDnEgZHJw/a5druS6p30YvsJKOde5VjeHiZqrMqk6I7F1EkbuBDjboX1pn+JfdaO7EqMzSiFGnNSzFJlbl5og1Zyq1jKpHWvnjX6r14VzzSfyWqOwCteGEBpnrl3tLFcLKbm9SAkY078GZoMPUqEKhWe8NIBvFgalVSUlaz4C5ltgBToyKk6uYOVYTfBkx9ytZG1gPGDkxdCh0rR6mmuKfB7MDUozS2In6CyYW+zBJMLYqpK1XPjGcrBrMGU/3y+0rpBEQxyXezB1P5CxvUQDX2aQcwFb/iY2lbyffdXMDU51IY9D0WjmDqco1QKPDXlbqB+S9cPGUHphZXlcFLskrmCqayl9vFX5fb4XyBD3MGU9XrEAef1yEurC7QfnUGQ3uBpsVs4Mce3cFgdCJFhkom9GpvV666gqG8pNdmnvRilw0CMITXOmPCsl5N8yVZgCG8CBxYbte3/SYCQ3d1PChnxr89a6YYKzBkYQjUJUjerK8bMJZggjXJVg9/1aUP+6EbMLZgaHQisAIDnu2ndsBYg5Gr6zqQOf3C1Gto1moXMJmKzJYGrCvr0X5pPyQXMDQ6EauLuwnt0sDFBUwwthHKH5HBB9zorPti4uIEBlgPrcBY8puso0j73TBycQNDoxMRo+d/qLqKsZ+XLf28SwAGU/A5h0zUePm7ZDbp40vLPFzcwQQDigCNiL61Wq2X5x8l2fOv/b/LGS0UYII+hU5kT6Zx0WiWZgVQSMAEfYowxIFMpcwdTNClCENUjgwBGBqdSNXIUICh0YlUjcwLBRiSAE21yDQuScCQ6EQqRab5QAMmuDozMq1XIjAkOpEKkWkh5Wo5RhGgqQwZmkXpwxYEYYiqkGk9EoIhCUNUhMw1JReaM/9KkGl9pwUTdDfuG5oKkGkSbWJk27lPwScn07jA6/iK7cZdDnFiMo2mn0P1HvoqzWqRaX7zJjZId65oTkem0br08R19orkL3diciEyjdU3mCpisfTdi/CClFG+n1GjbkynZmq3Wi3csB+vObjvz+XaSRDa2uizZHl6VyeVfPfgkISZjQFYAAAAASUVORK5CYII='; var hideStyle = document.createElement('style'); hideStyle.type = 'text/css'; hideStyle.innerHTML = `.hideButtonTitle { display: none !important; }`; document.body.appendChild(hideStyle); // tooltipを消す var hideTootipStyle = document.createElement('style'); hideTootipStyle.type = 'text/css'; hideTootipStyle.innerHTML = `.MuiTooltip-popper { display: none; }`; document.body.appendChild(hideTootipStyle); // mobile var mobileLike = document.querySelector('div.it-ActionsMobile_like'); var mobileLikeStyle = document.createElement('style'); mobileLikeStyle.type = 'text/css'; mobileLikeStyle.innerHTML = `.mobileLike { background-color: white !important; border: 2px solid rgb(93, 112, 124) !important; }`; document.body.appendChild(mobileLikeStyle); mobileLike.classList.add('mobileLike'); mobileLike.addEventListener('click', function() { setTimeout(function() { mobileLike.classList.add('mobileLike'); }, 0); }); mobileLike.addEventListener('mouseover', function() { setTimeout(function() { mobileLikeButton.classList.add('hideButtonTitle'); mobileLikeButton.title = 'Google Tag なんとか'; mobileLikeButton.classList.remove('hideButtonTitle'); }, 0); }); var mobileLikeButton = mobileLike.querySelector('button'); var buttonStyle = document.createElement('style'); buttonStyle.type = 'text/css'; buttonStyle.innerHTML = `.mobileLikeButton { display: flex; justify-content: center; align-items: center; }`; document.body.appendChild(buttonStyle); mobileLikeButton.title = 'Google Tag Manager'; mobileLikeButton.classList.add('mobileLikeButton'); var span = mobileLikeButton.querySelector('span:first-child'); span.parentNode.removeChild(span); mobileLikeButton.insertAdjacentHTML('afterbegin', '<img src="' + img + '" style="width: 45px; margin-right: 10px;" />'); var mobileCount = mobileLikeButton.querySelector('span:last-child'); var countStyle = document.createElement('style'); countStyle.type = 'text/css'; countStyle.innerHTML = `.likeCount { color: rgb(93, 112, 124); }`; document.body.appendChild(countStyle); mobileCount.classList.add('likeCount'); // footer var footerLike = document.querySelector('div.it-Footer_like'); footerLike.title = 'Google Tag Manager'; footerLike.addEventListener('mouseover', function() { setTimeout(function() { footerLike.classList.add('hideButtonTitle'); footerLike.title = 'Google Tag なんとか'; footerLike.classList.remove('hideButtonTitle'); }, 0); }); var footerButton = footerLike.querySelector('button'); var footerButtonStyle = document.createElement('style'); footerButtonStyle.type = 'text/css'; footerButtonStyle.innerHTML = `.footerLikeButton { background-color: transparent !important; border: none !important; }`; document.body.appendChild(footerButtonStyle); footerButton.classList.add('footerLikeButton'); footerButton.innerHTML = '<img width=45 src="' + img + '" />'; var footerCount = footerLike.querySelector('a'); footerCount.classList.add('likeCount'); };manifest.jsonの設定を書きます。
gtm/manifest.json{ "name": "GTM", "version": "1.0.0", "manifest_version": 2, "description": "GTM Chrome Extension", "content_scripts": [{ "matches": ["https://qiita.com/**/items/*"], "js": [ "gtm.js" ] }] }早速戻してみましょう・・・おっと、サービスを間違えたようです
これは酷い
- 投稿日:2020-03-14T22:52:29+09:00
【8日目】JavaScript 配列操作のメソッド コールバック関数
本日の学び
こんばんは。
今日はホワイトデーでした。
午前中はお菓子を作り、
昼過ぎからプログラミングです。
思ったよりダラダラしてしまった。反省。本日も学びのアウトプット。
- Progate Javascript 学習コース6
- Progate Javascript 学習コース7
//pushメソッド const characters = ["にんじゃわんこ", "ベイビーわんこ", "ひつじ仙人"]; //定数3つ console.log(characters); characters.push ("とりずきん"); //定数の追加 console.log(characters); //forEachメソッド(因数の中身を全部使う) コールバック関数(因数の中の関数) const characters = ["にんじゃわんこ", "ベイビーわんこ", "ひつじ仙人", "とりずきん"]; characters.forEach((character)=>{ //コールバック関数 console.log(character); }); //findメソッド filterメソッド(findに似てる) const numbers = [1, 3, 5, 7, 9]; const foundNumber = numbers.find((number)=>{ return number%3 === 0}); //条件に当てはまるものを探して新しい関数名に代入 console.log(foundNumber); //mapメソッド(すべての要素に反映させる) const numbers = [1, 2, 3, 4]; const doubledNumbers= numbers.map((number)=>{ //numberの要素に対して条件を与える return number*2; }); console.log(doubledNumbers); //コールバック関数(引数に関数を渡す)→よく分かっていない。要ググる const printWanko = () => { console.log("にんじゃわんこ"); }; const call = (callback) => { //printwankoになる console.log("コールバック関数を呼び出します。"); callback(); //コールバック関数を呼び出す }; call(printWanko); //コールバック関数(callbackにprintWankoを代入) //コールバック関数の直接定義 const printWanko = () => { console.log("にんじゃわんこ"); }; const call = (callback) => { //callbackとprintwankoの情報となる console.log("コールバック関数を呼び出します。"); callback(); //コールバック関数を呼び出す }; call(printWanko); //printwankoをcallに代入 call(() => { //代入の中身 console.log("ひつじ仙人"); });関数(いくつかの情報をまとめたもの)
引数(関数に情報を追加するもの)
コールバック関数(引数に関数を渡す)・・・?
関数の中に引数を入れる認識でいました。
でもコールバック関数は立場が逆?
なんだかよく分かりませんでした。明日、ググってみてもう少し調べようと思います。
今日の所感
とりあえずprogateのJavascriptは終了しました。
ほんとにこれだけでいいのか不安ではありますが、
とりあえず先に進みたいと思います。
明日からはJQueryを勉強していこうと思います。あと感じましたが、夜の勉強って効率が悪い気がしてきました。
少し早めに寝て、朝勉強する習慣に切り替えていこうと思います。
- 投稿日:2020-03-14T22:18:41+09:00
reglとReactでWebGLのシェーダを簡単に扱う
reglとは
WebGLでお絵描きをしたいとき、大きく分けて以下の2つの方向性がある。1
- three.jsなどの高レベルなライブラリを使う
- GLSLでシェーダを記述し、WebGL APIをそのまま扱う
後者のようにGLSLを扱えると自由度が高い一方で、WebGL APIに関連して "おまじない" 的なコードが大量に必要になってしまうという問題があった。
この記事で扱う regl は、WebGLのwrapperに相当する。初見でもWebGL APIと1:1に対応づけられる程度には原型を維持することで自由度を担保しつつ、大量の "おまじない" や煩雑な状態管理を単純化し、コードの可読性を大幅に向上してくれる。
公式GitHubでは
Functional WebGL
と謳っているが、おそらく「変数を入れると(GLSLに基づいて)描画が得られる」ことを指してFunctional
と言っているのだと思われる。ReactでいうところのstatelessなFunction Componentみたいなイメージか。Reactでreglを扱う
GitHubにいくつかサンプルコードがあるものの、Reactで扱っている例はググってもあまり見当たらなかった。Reactでのシンプルなコードを以下に記載する。
このコードには基本的なシェーダの扱いとアニメーションの方法が含まれており、静止した三角形の色が時間とともに変化する。
Samnple.jsimport React, { useEffect } from "react"; import createRegl from "regl"; const Sample = () => { const regl = createRegl(); // No arguments: create a full screen canvas const drawTriangle = regl({ frag: ` precision mediump float; uniform vec4 color; void main() { gl_FragColor = color; }`, vert: ` precision mediump float; attribute vec2 position; void main() { gl_Position = vec4(position, 0, 1); }`, attributes: { position: [[0, -1], [-1, 0.5], [1, 1]] // No need to flatten }, uniforms: { color: regl.prop("color") }, // Number of vertices to draw in this command count: 3 }); useEffect(() => { regl.frame(({ time }) => { regl.clear({ color: [0, 0, 0, 0], depth: 1 }); drawTriangle({ color: [ Math.cos(time * 1.0), Math.sin(time * 0.8), Math.cos(time * 3.0), 1 ] }); }); return () => { regl.destroy() }; // Clean up when unmounted }, []); return <></>; }; export default Sample;ポイント
初期化
const regl = createRegl()
のところで引数をしていないため、全画面のcanvasが新たに作成される(reglの仕様)。
既存のcanvasを使用したい場合等は引数で指定できる(公式ドキュメント参照)描画
drawTriangle()
は、frag
,vert
に文字列として記載されたGLSLに基づいて三角形を描画する関数。ここで、color: regl.prop("color")という部分に注目。
drawTriangle()
に引数color
を渡すことで動的に描画内容を変えられるようになっている。副作用フック
useEffect
によって、Reactコンポーネントの副作用として描画を実行している。
useEffect
の第二引数が[]
なので副作用は一度だけ実行されるが、その中でregl.frame()
が呼ばれることによりアニメーションが開始する。副作用のclean up
return () => { regl.destroy() };
useEffect
のなかで上記のようなreturn
文がある。今回はcomponentと無関係に全画面のcanvasが作成されているので、このように副作用のclean upを行わないと、別コンポーネントに画面表示を切り替えた後にもcanvasが残ってしまう。※ そもそもコンポーネント外にcanvasを作成するのは良くないので、component内に予め用意したcanvasをcreateReglに渡すのが良い気がする
three.jsでシェーダを扱う方法もあります ↩
- 投稿日:2020-03-14T21:04:00+09:00
WordPressテーマ作成法
はじめに
WordPressのテーマ(テンプレート)作成方法について、ざっくりとまとめてみます。
今後、随時追記予定です。最低限必要なファイル(ページ)
WordPressのテーマを作るにあたり、最低限必要となるのは以下のファイルです。
- index.php
- style.css
なお、
style.css
には、以下の情報を記入します。style.css/* Theme name: テーマの名称 Theme URI: テーマのURI(ダウンロードページなど) Description: テーマの説明 Version: テーマのバージョン Author: テーマ作成者の名前 Author URI: テーマ作成者のホームページのURI */
一般的なファイル構成
上で書いたのは、「最低限」必要なファイルで、一般的には以下のようなファイル構成になることが多いです。
- front-page.php:ブログのトップページ
- single.php:個別の記事ページ
- page.php:固定ページ
- category.php:カテゴリーページやタグページ
- search.php:検索結果ページ
- archive.php:記事一覧ページ
- 404.php:404エラーページ
まとめ
ここでは、WordPressのテーマ作成法法についてざっくりとまとめてみました。
まだ書き切れていないところが多々あるので、今後も追記していきます。
- 投稿日:2020-03-14T21:00:28+09:00
魔法JS☆あれい 第1話「popでpushした、ような……」
登場人物
丹生(にゅう)あれい
魔法少女「魔法(マジカル)JS(女子小学生)☆あれい」として活動中。イテレー太
正体不明の魔法生物。pop()
イテレー太「あれい、すべての前置きを端折るけど、今戦闘の真っ最中だよ!」
あれい「楽してんじゃねえよこの駄犬が」
イ「犬じゃなくてイテレータ型魔法生物だよ! そして敵を倒すにはあれいの配列魔法が必要なんだ!」
あ「人の話を聞けこの雑種」
イ「僕が魔法世界から召喚したデータを、あれいの配列魔法を使って、敵の弱点に合わせた値に変換してreturn
する事で、ダメージを与えることができるんだ!」
あ「なら最初からまともなデータを用意しろよポンコツ」
イ「さて、今回僕が召喚したデータは……これだよ!」items = ['羽二重餅', '水ようかん', '梅月せんべい'];あ「……それ、全部福井の銘菓じゃねえか。相変わらず福井県民丸出しだな」
イ「そして、今回の敵の弱点は、『配列の最後の要素』だよ! さあ、配列の最後の要素をreturn
してぶつけるんだ!」
あ「だから最後の要素だけ召喚しろっつってんだろこのボロ雑巾」
イ「頼んだよ! あれい!」
あ「面倒くせえなあ……」return items.pop();イ「おおっ! やったよ、あれい! 梅月せんべいが炸裂したよ! 梅月せんべいで敵をやっつけたよ!」
あ「お前ちょっと黙ってろ」解説
pop()
メソッドは、配列を操作するメソッドの中でも最も基本的なものです。
MDNによると……
pop()
メソッドは、配列から最後の要素を取り除き、その要素を返します。このメソッドは配列の長さを変化させます。つまり、積み重ねられたカードから、一番上に乗っているカードを1枚取って、そのカードを相手に返すようなイメージです。配列というスタック構造のデータの操作としては、最も直感的にわかりやすい動きですね。
push()
イ「おっと、次の敵が出てきたよ!」
あ「どうなってるんだこの街は」
イ「さて、僕が魔法世界から召喚したデータは、今こんな状態になっているよ!」items = ['羽二重餅', '水ようかん'];あ「そうだな」
イ「そして、新たに魔法世界から召喚したデータは、これだ!」newItem = '五月ヶ瀬';イ「このデータを、元の配列の最後に追加した上で、その最後の要素を敵にぶつけるんだ!」
あ「……それこそ直接ぶつけりゃいいじゃねえか。バカなのかお前は」
イ「いいから! 急いで、あれい!」
あ「面倒くせえなあ……」return items.push(newItem);あ「……おい、効いてないぞ」
イ「わははは、引っかかったね!push()
メソッドの戻り値は、配列の要素の数なんだよ!」
あ「お前どっちの味方なんだよ」解説
push()
メソッドも、最も基本的な操作の一つですね。
またまたMDNによると……
push()
メソッドは、配列の末尾に 1 つ以上の要素を追加することができます。また戻り値として新しい配列の要素数を返します。つまり、積み重ねられたカードの上に、さらにカードを乗せるようなイメージです。これも直感的にわかりやすい動きですね。
また、このメソッドの戻り値は、配列の要素の数です。例えば、空の配列に1つの要素をpush()
した場合、戻り値は1になります。
イ「さて、ピンチだよ! どうする、あれい!」
return items[items.push(newItem)-1];あ「これでいいのか」
イ「あ、うん……攻撃できたね」
あ「だからどっちの味方なんだよ」return items.slice(0, items.push(newItem)).pop();あ「これでもいいのか」
イ「あ、slice()
とかはもっと後の話で出てくるからまだやめて……」
さて、魔法JS☆あれいは、魔法(JavaScript)の力でこの世界を守ることが出来るのか!? 次回に続く!
- 投稿日:2020-03-14T20:28:25+09:00
一家に一枚 FF の表
2 3 4 5 6 7 8 9 a b c d e f 1 ---------------------------------------- 2| 4 6 8 a c e 10 12 14 16 18 1a 1c 1e 3| 6 9 c f 12 15 18 1b 1e 21 24 27 2a 2d 4| 8 c 10 14 18 1c 20 24 28 2c 30 34 38 3c 5| a f 14 19 1e 23 28 2d 32 37 3c 41 46 4b 6| c 12 18 1e 24 2a 30 36 3c 42 48 4e 54 5a 7| e 15 1c 23 2a 31 38 3f 46 4d 54 5b 62 69 8| 10 18 20 28 30 38 40 48 50 58 60 68 70 78 9| 12 1b 24 2d 36 3f 48 51 5a 63 6c 75 7e 87 a| 14 1e 28 32 3c 46 50 5a 64 6e 78 82 8c 96 b| 16 21 2c 37 42 4d 58 63 6e 79 84 8f 9a a5 c| 18 24 30 3c 48 54 60 6c 78 84 90 9c a8 b4 d| 1a 27 34 41 4e 5b 68 75 82 8f 9c a9 b6 c3 e| 1c 2a 38 46 54 62 70 7e 8c 9a a8 b6 c4 d2 f| 1e 2d 3c 4b 5a 69 78 87 96 a5 b4 c3 d2 e12 の段は覚えよう!
f の段は1の位と10の位の数字の和が f になるから覚えやすい。
f × f = e1
e + 1 = f8 の段も覚えられそう。
1 × 1 から f × f への並びは覚えておくと楽しいことがあるかも。
1の位
149094101490941
10の位
000112345679aceちなみにプログラム
ff.jsconst psw = _ => process.stdout.write( _ ) psw( ' 1' ) for ( let r = 2; r < 16; r++ ) psw( ' ' + r.toString( 16 ) ) psw( '\n' ) psw( ' 1 ----------------------------------------\n' ) for ( let l = 2; l < 16; l++ ) { psw( l.toString( 16 ) + '|' ) for ( let r = 2; r < 16; r++ ) { let _ = ' ' + ( l * r ).toString( 16 ) psw( _.slice( _.length - 3 ) ) } psw( '\n' ) }$ node ff.js
- 投稿日:2020-03-14T19:35:03+09:00
JavaScript初学者の私がFizzBuzzアプリを自作して学んだこと
はじめに
私はフロントエンドエンジニアへの就職を目指して、現在4ヶ月程独学でプログラミングを学習している初学者です。
学習内容のアウトプットを行うために、自由に数値を入力することが可能なFizzBuzzアプリを作成しました。このアプリを作成するに当たって参考にした記事はこちらになります
【実体験】未経験からフロントエンド開発を目指す!効率良く就活フェーズへ入る為のロードマップ【課題3種付き】
こちらの記事では未経験者がフロントエンドエンジニアに転職するまでのロードマップが課題と共にわかりやすく解説されていますので、ぜひ参考してみて下さい。学んだことのアウトプット・私と同じようなJS初学者の参考になればと思いこの記事を作成しています。
この記事で分かること
- アプリ作成の流れ
- 私が行き詰まった点
アプリ作成の流れ
1、追加したい機能を書き出す
2、その機能がどのようなコードで実装できるか大まかに予測する
3、実際にコードを書くまず初めに自分が作りたいアプリに必要な機能の洗い出しを行いました。
具体的に今回のアプリを例にとって書き出すと
- FizzBuzzの計算式
- 入力された整数値を読み取る
- 計算結果を表示する
などです。
この時最初から全ての機能を洗い出せる必要はなく、後から足りないと分かれば足していけばいいと思います。
次に行うのは、洗い出した機能をどうすれば作ることができるのか文章で書くことです。
具体的に言うと、
- FizzBuzzの計算式→if文を使ってFizz・Buzz・FizzBuzzの条件を分ける
- 入力された整数値を読み取る→inputに入力された値を変数に保存する
- 計算結果を表示する→documentにtextcontentなどを使って表示する
この時点で自分が見当がつく処理の仕方を少しだけ具体的に書いておきます。
なぜこのような一見手間なことをするのかと言うと、
1.個別の機能を一つずつ作成していくことができる
2.自分の中でどのような手順で作っていけばいいのか考えやすくなる以上の理由からこのような工程を行なっていきます。
※完了した作業は✔︎を入れるとどこまで作業が完了しているのか分かるのでおすすめです!!!!
私が行き詰まった点
1、入力欄の値を取得する
結論から言うと、valueを使えばいいという簡単な話なのですが、この結論に行き着くまでにかなりの時間がかかりました。
というのも定数や変数にinputに入力した値を保存するという概念がなかったのでvalueをどこに記述すればいいのかが分からず、ググりつつあれこれ自分でコードをいじりながら一つずつ試していきました。
valueの使い方を念のため記載しておきます。JavaScriptlet form1 = document.getElementById('fizznumber'); let fizz = form1.value;2、追加した要素を削除する
追加した要素というのは、documentに表示した計算結果のことです。
innerhtmlでdocumentに計算結果を追加する機能は簡単に実装することができましたが、再度入力した時に前回の計算結果が表示されたまま次の計算結果を表示してしまう問題にぶつかりました。この問題点も個人的に大きく時間がかかりました。
innerhtmlを空にするという方法が簡単にググれば出てくるのですが、具体的に自分で書いたコードのどの箇所にそれを記述するのかが分からず、この問題は自分では解決できないと判断したためテラテイルで質問して解決しました。まとめ
実際に自分で一からコードを書いて何か成果物を作るのはとても大変で時間がかかりましたが、大きな自信になりました。
また、コードを書いていてエラーにぶつかることはとてもストレスになりますが、今の自分にはないスキルを身につける大きなチャンスにもなりこれからもどんどんコードを書いていきたいと思いました。
と技術的な点には全く触れずブログのような記事になってしまいましたが、これにて終わりにしたいと思います。
- 投稿日:2020-03-14T18:44:01+09:00
Firebase Realtime Database を Cocos Creatorで使う。
はじめに
初投稿です。こんにちは。
昨今ではクロスプラットフォームのゲームエンジンといえば Unity 一択で、
次点でどうにかUEが出てくる程度じゃないかと思います。
そんな中 cocos creator に手を出したのはいいものの、情報が少なく割と困ったので、備忘録がてら投稿します。したいこと
cocos creator で開発中のアプリで Firebase の Realtime Databaseにアクセスする。
方法
cocos creator は Node.js を利用しているので基本的には npm を利用する。
ただし、この方法ではAndroid上では動作しなかった。(iOSやWeb上では問題ない)
仕方なくAndroid StudioでFirebase Databaseを導入し、それをJavaを通じて呼び出すという手法をとりました。npm install --save firebase
Cocos creator のプロジェクトフォルダでターミナル等を使って
npm install --save firebaseを実行する。
これについては以下の記事が非常に参考になりました。
Cocos Creator を使って Firebase Authentication を実装する最初はうまくモジュールを読み込めていなかったのですが、Cocos Creator を再起動するとうまく読み込めたようです。
あとは Node.js でFirebase を利用するやり方で問題なく実行できます。
Firebase を JavaScript プロジェクトに追加するAndroid
Androidでは何故かモジュールを利用できません。Node.js の npm を利用しているという都合上、何か問題が起きているのかもしれないです。(把握できてない)
上記の記事では少なくとも Firebase Authentication については問題なく実行できていますが、自分の環境では Firebase Realtime Database は使用できなかったので仕方なく Java を直接利用することにしました。まずはプロジェクトに Firebase を導入する必要があります。
Android プロジェクトに Firebase を追加する
そうして導入した Firebase Realtime Database を Java を通じて利用します。
cocos creator から javascript で Java を利用するにはjsb.reflection.callStaticMethod(className, methodName, methodSignature, parameters...)を使用します。
How to Call Java methods using JavaScript on Android具体的には org.cocos2dx.javascript に使いたい Java のパッケージを置き、それを javascript から呼び出すといった形です。最初は org.cocos2dx.javascript の場所がわからなかったので一応書いておくと、
/Project_Folder/build/jsb-default/frameworks/runtime-src/proj.android-studio/src/org/cocos2dx/javascript/
にあります。そこに、
FirebaseWrapper.javapackage org.cocos2dx.javascript; import android.util.Log; import com.google.firebase.database.FirebaseDatabase; public class FirebaseWrapper { private static FirebaseDatabase database=null; public static void init(){ if(null==database){ database=FirebaseDatabase.getInstance(); Log.d("Firebase Wrapper","Firebase Initialized!"); } } }みたいなものを適当にでっち上げました。
これを実行するには javascript 上でjsb.reflection.callStaticMethod("org/cocos2dx/javascript/FirebaseWrapper","init","()V")を呼びます。
なおresult=jsb.reflection.callStaticMethod()
は残念ながら引数、返り値ともに渡せるものが限られています。
具体的には、int, float, boolean, String しか渡せません。なので、配列などを渡すことはできません。一つずつ渡すなど何か工夫が必要です。とはいえ一先ずこれで無事 Firebase Realtime Database にはアクセスできます。
終わりに
今回、cocos creator を使って初めてアプリを作ったのですが、自分の技術不足もあり、正直とてもしんどかったです。
まだまだ情報不足の面が多く、基本的に英語か中国語の情報を漁るしかないので素人は手を出すべきでなかったと感じました。
ただそれゆえに情報が増えづらいといった面もあるので、未来の誰かか自分の為にもこの記事を投稿しておきます。いろいろ勉強にはなりましたが次はたぶん素直に Unity を使うと思います。笑
- 投稿日:2020-03-14T18:36:03+09:00
Vue.jsの$emitでpromiseしたいでござるの巻
先日書いた記事で、出来るだけtemplateで$emitした方が良いと書きました。
<template> <dl> <dt>名前入力</dt> <dd> <input type="text" @input="$emit('customEvent', hoge('hogehoge'))" /> </dd> </dl> </template>こういう感じにemitは直接templateから指定して、引数からメソッドを呼んで必要に応じて成型した戻り値を親コンポーネントへ送ります。
みたいなことを書いたわけですが、$emit時に返す値がAPIの読み込みなどの非同期な場合に上手くいかない事がありました。
なのでそういう場合は
hoge(e){ axios.get('fuga', e).then(res => { this.$emit('fuga', res.data) }) }みたいな感じに、イベントから呼び出した関数内でemitするパターンになってしまうのですが…。
async hoge(e){ const param = await axios.get('fuga', e).then(res => res.data) // 実際にはここでparamの加工とかする return param }とか言う感じでpromiseで返してあげると
<template> <div id="app"> <SampleComponent @customEvent="fuga" /> </div> </template> <script> export default { components: { SampleComponent }, methods:{ fuga(e){ e.then(result => alert(result)) } } } </script>親側で受け取った際に、引数にPromiseオブジェクトで帰ってくるので、引数に
then
とか付けれました。
まぁ、よく考えれば、そりゃそうだろうって感じですが…。<button @click="hoge().then(res => $emit('hogehoge', res))">TEST2</button>ちなみに、Promiseオブジェクトで送るのがなんか嫌でこう書いたら動くんですが、vue側でエラーを出してきます。
[Vue warn]: Property or method "then" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
こんな感じのエラーになりました。英語に疎いのでちょっと意味が…。
とまぁ、何の役にも立ちそうにない小話です。
できれば$emitがpromiseにも対応してくれればいいなぁと思った今日この頃です。
- 投稿日:2020-03-14T17:51:18+09:00
webサービスを運営してみた(2020/3/14)
はじめに
アルバイトの勤怠管理を無料でできるサービスTimestampを個人で運営しています。
アクティブなアカウント数0人の当サービスでまずは売上1円を上げることが目標です?
フルタイムで仕事をしながら個人でサービス運営できるかの実験だったり技術向上が目的だったりしていて
ここでは運営や開発に関する記録を残していきます。ユーザー数
googleアナリティクスから過去1ヶ月のユーザ数の推移です。
3月8日にユーザが伸びているのはクラウドワークスに依頼を掲載したからですね。
海外からちょこちょこアクセスされて驚きました。
あいかわらずアクティブなアカウント0?雑記
LPデザインを外注しました
LPデザインをクラウドワークスで外注してみました。
結構な数の応募がありまして「高校生で実績もないので格安で構いません」とか「ドバイで会社を立ち上げて頑張ってます!」みたいな人もいましたね。
金額は5万円で設定しているのですが、これぐらいの出費なら個人開発でも許容範囲じゃないでしょうか。
個人開発に外注を交えるのは現実的な手段に思えます。リリース情報
- アカウントを登録しなくてもサービスを試せるようにしました。こちらからどうぞ。
- アルバイトのシフトを作成できる機能をリリースしました。
- 投稿日:2020-03-14T17:44:55+09:00
Jsの演算子を備忘録
Jsで詰まったところを残しておくメモ。
その1
この2つの式は同じ。
javascriptvar intA = 399; var B = !(intA % 4);javascriptvar intA = 399; var B; if(intA % 4 === 0){ B = true; }else{ B = false; }よく調べたってわけじゃあないが、どうやら、
!( )
とするとtrueかfalseかを返すようになるらしい。
ちなみにここでは、余りが0ならtrue、余りが出たらfalseだった。その2
この2つの式は同じ。
javascriptvar hensu = a && b && c;javascriptvar hensu; if(a == false){ hensu = a; }else if(b == false){ hensu = b; }else if(c == false){ hensu = b; }aがtrue(と、みなせる値)なら次に進み、false(と、みなせる値)が出たらその時点での値を返す。
必ずしもtrueかfalseで落ち着くわけじゃない。
たとえば、var hensu = (a > 20);
という式の場合、hensu = a
ではなくhensu = (a > 20)
を返すからtrueかfalseになるわけ…らしい。その3
この2つの式は同じ。
javascriptvar c = a == 15 && 'red' || 'not_both';javascriptvar c; if(a == 15){ c = 'red'; }else{ c = 'not_both'; }その4
この2つの式は同じ。
こちらの記事から引用。。。ごめんなさい。。。javascript// 変数 name の値がtrueと評価されるならばname, // そうでなければ文字列'anonymous'が変数playerにセットされる // 条件分岐省略前 var player; if (name) { player = name; } else{ player = 'anonymous'; }javascriptvar player = name || 'anonymous';その5
この2つの式は同じ。
こちらの記事が秀逸な解説だったのでそちらから引用。。。javascriptvar result; if(str === 'piyo'){ result = 'piyo!!'; } else { result = 'not piyo…'; }javascriptvar result; result = str === 'piyo' ? 'piyo!!' : 'not piyo…'その6
カンマの使い方。
javascript// 一気に変数を定義できる!(中身はundefinedになります) var a, b, c, d; // 最後の行に ; がついてさえいれば、 // 途中のコードの文末を,にしても大丈夫! console.log(a), console.log(b), console.log(c), console.log(d); // 値を仕込みたい場合は = でつなぐ var e = f = g = h = 'marufoi! '; console.log(e + f + g + h); a = 1; b = 2; c = 3; // 代入が優先されるので、1が入る var kon01 = a, b, c; console.log(kon01); // 1 // 左から順番に実行(するので、3が入る) var kon02 = (a, b, c); console.log(kon02); // 3参考文献
https://qiita.com/smicle/items/7d3b9881834dc0142fb7
https://qiita.com/Imamotty/items/bc659569239379dded55
https://qiita.com/amamamaou/items/785d1fa6f32d5586ed49
https://teratail.com/questions/38967
- 投稿日:2020-03-14T17:21:45+09:00
react-reduxを読む - 更新処理編
はじめに
前回はconnect関数が返すConnectFunctionのうち、初めに表示される際の処理について見てきました。今回はReduxのStateが更新される際にどのような動作が行われ、表示が更新されるのかについて見ていきましょう。
Reduxの更新処理
Reduxについてはご存知という前提ですが簡単に確認しましょう。Reduxには以下の要素があります。
- Store
Stateを保持するオブジェクト。- Action
Storeに対する変更要求。Storeのdispatch
メソッドを使って送信する。- Reducer
現在のStateとActionから次のSTateを作成する関数。この記事ではあまり出てこない。またStoreにはsubscribeメソッドがあり、Actionがdispatchされたら呼び出されるコールバックを登録することができます。このsubscribeが今回の鍵となります。
Provider
前回も少し見たProviderコンポーネントから始めましょう。
Provider.js抜粋function Provider({ store, context, children }) { const contextValue = useMemo(() => { const subscription = new Subscription(store) subscription.onStateChange = subscription.notifyNestedSubs return { store, subscription } }, [store]) const previousState = useMemo(() => store.getState(), [store]) useEffect(() => { const { subscription } = contextValue subscription.trySubscribe() if (previousState !== store.getState()) { subscription.notifyNestedSubs() } return () => { subscription.tryUnsubscribe() subscription.onStateChange = null } }, [contextValue, previousState])まずSubscriptionオブジェクトが作られています。この中身はこの後で見ていきます。
その次のuseEffectはまたよくわからない書き方です。このuseEffectはuseMemoと同じようにReactが提供する関数ですが、少し毛色が異なります。useEffectは副作用のある処理を行いたい場合に使うようです。ドキュメントにあるように「データの購読(英語ページだとsubscriptions)」は副作用があるためuseEffectを使う必要があるようです。useEffectを含めたuseシリーズはReact 16.8で導入されたフックAPIです。フックは「クラスを使わずに関数で、クラスを定義して行っていたState管理(ReduxのではなくReact本体のstate)等を実装する機能」です。
その中でuseEffectはクラス定義のコンポーネントで言うcomponentDidMountとcomponentWillUnmount に相当するものだそうです。「useEffectに渡す関数」がcomponentDidMount、「useEffectに渡す関数がreturnする関数」がcomponentWillUnmount。
「購読の開始と解除を並べて書けるからいいでしょ」とドキュメントに書かれてますが、個人的にはインデントレベルが変わるのが微妙…(それとuseEffectのことを知らない人が見たときに理解するのに時間がかかる)という気がします。Subscription
さて、Subscriptionに移ります。utils/Subscription.jsに定義されています。
Subscription.js抜粋export default class Subscription { constructor(store, parentSub) { this.store = store this.parentSub = parentSub this.unsubscribe = null this.listeners = nullListeners this.handleChangeWrapper = this.handleChangeWrapper.bind(this) } trySubscribe() { if (!this.unsubscribe) { this.unsubscribe = this.parentSub ? this.parentSub.addNestedSub(this.handleChangeWrapper) : this.store.subscribe(this.handleChangeWrapper) this.listeners = createListenerCollection() } } handleChangeWrapper() { if (this.onStateChange) { this.onStateChange() } } notifyNestedSubs() { this.listeners.notify() }Providerコンポーネントで作られるSubscriptionオブジェクトはparentSubを渡していないのでStoreに対してsubscribeが行われます。Storeからコールバック(handleChangeWrapperメソッド)が呼ばれるとonStateChangeが呼ばれます。今の場合onStateChangeに設定されているのはnotifyNestedSubsです。自身のlistenerに対して変更があったことを伝えるという一般的なPub/Subモデルですね。
ここでnotifyされる対象は誰なのか、その後どう動くのか、ということについて調べるために、ConnectFunction関数の前回読み飛ばした部分に進みましょう。
ConnectFunction再び
ConnectFunctionではchildPropsSelectorを作った後に以下のコードがあります。githubでの表示はこちら。
connectAdvanced.js抜粋const [subscription, notifyNestedSubs] = useMemo(() => { if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY // This Subscription's source should match where store came from: props vs. context. A component // connected to the store via props shouldn't use subscription from context, or vice versa. const subscription = new Subscription( store, didStoreComeFromProps ? null : contextValue.subscription ) // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in // the middle of the notification loop, where `subscription` will then be null. This can // probably be avoided if Subscription's listeners logic is changed to not call listeners // that have been unsubscribed in the middle of the notification loop. const notifyNestedSubs = subscription.notifyNestedSubs.bind( subscription ) return [subscription, notifyNestedSubs] }, [store, didStoreComeFromProps, contextValue])Subscriptionオブジェクトが作られていますが今度は第2引数、つまりparentSubが渡されています(正確にはコンテキストのStoreを使う場合は、ということになりますが通常はコンテキストを使うでしょう)
このsubscriptionがどこで使われているか見ていくと以下のコードがあります。少し読み飛ばしており「この変数何?」というものがいますがそこについては後から説明します。
connectAdvanced.js抜粋// Our re-subscribe logic only runs when the store/subscription setup changes useIsomorphicLayoutEffectWithArgs( subscribeUpdates, [ shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, childPropsFromStoreUpdate, notifyNestedSubs, forceComponentUpdateDispatch ], [store, subscription, childPropsSelector] )useIsomorphicLayoutEffectWithArgsはconnectAdvanced.jsの上の方に定義されています。ブラウザでの実行かサーバサイドレンダリング(SSR)かで呼ぶ関数を切り替えるということが行われていますがまあ結局useEffect、つまりrenderした後に実行される関数を登録しているという点ではあまり違いはありません。ということでsubscribeUpdatesに進みます。
subscribeUpdates
subscribeUpdatesはconnectAdvanced.jsの上の方に定義されていますがこれもまた難解です。
connectAdvanced.js抜粋function subscribeUpdates( // 省略。上の配列に入ってるものが渡されてきます ) { // 省略 // We'll run this callback every time a store subscription update propagates to this component const checkForUpdates = () => { // 後で見ます } // Actually subscribe to the nearest connected ancestor (or store) subscription.onStateChange = checkForUpdates subscription.trySubscribe() // 省略 }上から読んでいくとややこしくなるのでまた先に構造を眺めてみました。今度はonStateChangeとしてsubscribeUpdates内に定義されているcheckForUpdatesが設定されています。
その後にtrySubscribeメソッド呼び出し。今度はparentSubがtruthy1なのでparentSubのaddNestedSubが実行されます。addNestedSubの先は淡々と頑張ってるだけなので省略します。Subscription.js抜粋trySubscribe() { if (!this.unsubscribe) { this.unsubscribe = this.parentSub ? this.parentSub.addNestedSub(this.handleChangeWrapper) : this.store.subscribe(this.handleChangeWrapper) this.listeners = createListenerCollection() } }以上のことからStoreにActionがdispatchされると次のように動作することがわかりました。
- Storeにdispatch
- Providerで作ったsubscriptionが呼び出される
- ConnectFunctionで作ったsubscriptionが呼び出される2
checkForUpdates
それではcheckForUpdate関数を見てみましょう。
connectAdvanced.js抜粋// We'll run this callback every time a store subscription update propagates to this component const checkForUpdates = () => { if (didUnsubscribe) { // Don't run stale listeners. // Redux doesn't guarantee unsubscriptions happen until next dispatch. return } const latestStoreState = store.getState() let newChildProps, error try { // Actually run the selector with the most recent store state and wrapper props // to determine what the child props should be newChildProps = childPropsSelector( latestStoreState, lastWrapperProps.current ) } catch (e) { error = e lastThrownError = e } if (!error) { lastThrownError = null } // If the child props haven't changed, nothing to do here - cascade the subscription update if (newChildProps === lastChildProps.current) { if (!renderIsScheduled.current) { notifyNestedSubs() } } else { // Save references to the new child props. Note that we track the "child props from store update" // as a ref instead of a useState/useReducer because we need a way to determine if that value has // been processed. If this went into useState/useReducer, we couldn't clear out the value without // forcing another re-render, which we don't want. lastChildProps.current = newChildProps childPropsFromStoreUpdate.current = newChildProps renderIsScheduled.current = true // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render forceComponentUpdateDispatch({ type: 'STORE_UPDATED', payload: { error } }) } }ちょっと長いですが略すところがないので。2行にまとめると以下のようになります。
- StoreからStateを取得してSelectorを実行
- propsに変化があるようならforceComponentUpdateDispatchを実行
次の話題はforceComponentUpdateDispatch(とchildPropsFromStoreUpdateとか)とは何者なのかです。ここで飛ばした部分が出てきます。
三度ConnectFunction - useRefとuseReducer
先にchildPropsFromStoreUpdateから。ConnectFunctionに戻るとこれらは以下のように定義されています。
connectAdvanced.js抜粋// Set up refs to coordinate values between the subscription effect and the render logic const lastChildProps = useRef() const lastWrapperProps = useRef(wrapperProps) const childPropsFromStoreUpdate = useRef() const renderIsScheduled = useRef(false)useと言ったらReactフック、ということでuseRefもご多分に漏れずReactが提供する関数です。意味合いとしてはクラスにおけるインスタンス変数みたいな機能を提供するもののようです。
forceComponentUpdateDispatchはもう少し上で定義されています。
connectAdvanced.js抜粋// We need to force this wrapper component to re-render whenever a Redux store update // causes a change to the calculated child component props (or we caught an error in mapState) const [ [previousStateUpdateResult], forceComponentUpdateDispatch ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)useReducerは少し複雑です。ドキュメントにあるように動作としてはReduxのReducerと同じような感じです。
大事なのは、戻り値の二つ目で返されているdispatch(上のコードではforceComponentUpdateDispatchに代入されている)です。ドキュメントには明言されていませんが、dispatchを呼び出すことによりレンダリングのやり直しが行われるようになっています。diapatchの先については、いわゆる「本書の範囲を超える」内容、React内部の話となるのでreact-redux読解はこれにて終了となります。
更新時処理のまとめとあとがき
以上、更新時の処理を見てきました。Storeのsubscribeを使い、Reactのフックを駆使し、まさに「間をつなげる」にふさわしい処理が行われていました。プログラミング技術と言うか、「ReactのフックAPIはこう使え!」の見本みたいな感じでしたね。
ちなみに、Reactにフックが導入されたのは本文中にも書いたように16.8、2019/2/6です。当時Twitterで「クラス定義コンポーネントよさようなら」みたいなことを言ってる人を見た気がしてなんのこっちゃと思ってたのですが3こういうことだったんですね。まあでもフックは難しいので初心者はクラスから入るべきだと思います。
react-reduxもReactにフックが入ったことで書き直されたものがv7だということは更新時処理を本格的に眺め始めてから気づきました(ところでReact 16.8より前はどう実装されてたの?と)。いろいろな縁で(?)非常にJavaScriptらしい関数使いまくりなコードに巡り合えた気がします。
- 投稿日:2020-03-14T17:21:26+09:00
react-reduxを読む - 初回表示編
はじめに
状態管理ライブラリであるReduxとUIライブラリであるReactをつなぐreact-redux、その中でも特にconnect関数はJavaScriptに慣れている人でも「何これ( ゚Д゚)」という書き方であり、やってることもかなり黒魔術的です。この記事ではそんなconnect関数の中身に踏み込みいつものように「明日使えるかもしれなプログラミング技術」を学ぶことを目的とします。
なお一記事で書こうと思ったのですが予想以上に長くなったので記事を分けます。
- 初めに表示されるときの動き(この記事)
- 状態が更新されるときの動き
記事中で参照・引用しているreact-reduxのバージョンは7.2.0です。
react-reduxのconnect関数
公式ドキュメントのQuick Startを見るとconnect関数の使い方は以下のようになっています。
export default connect( mapStateToProps, mapDispatchToProps )(Counter)
connect(mapStateToProps, mapDispatchToProps)
で一度カッコが閉じられ、改めて(Counter)
となっています。connect(mapStateToProps, mapDispatchToProps, Counter)
ではありません。何故このようなややこしい書き方になっているかについてはAPIリファレンスのconnect関数の戻り値に書かれているサンプルコードを見ると納得できます。
// first call: returns a hoc that you can use to wrap any component const connectUser = connect( mapState, mapDispatch ) // second call: returns the wrapper component with mergedProps // you may use the hoc to enable different components to get the same behavior const ConnectedUserLogin = connectUser(Login) const ConnectedUserProfile = connectUser(Profile)つまりconnect関数で作ったhocを使って異なるコンポーネントに対して同じようにstateとdispatchのマッピングをpropsとして設定することができるようです。
なるほど完全に理解した。
ところでhocって何。Reactの方のドキュメントより。
Higher-Order Component(高階コンポーネント)高階コンポーネントとは、あるコンポーネントを受け取って新規のコンポーネントを返すような関数です。
コンポーネントがpropsをUIに変換するのに対して、高階コンポーネントはコンポーネントを別のコンポーネントに変換します。高階関数のように「コンポーネントを受け取るコンポーネント」といった感じでしょうか。ともかくconnect関数は「関数を返す関数」のようです。
connect関数
それではconnect関数の中身に踏み込んでいきましょう。
ルートディレクトリのindex.jsによるとconnect関数が定義されているのはconnect/connect.jsのようです。connect.js抜粋export default /*#__PURE__*/ createConnect() export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) { return function connect( mapStateToProps, mapDispatchToProps, mergeProps, { // 省略 } = {} ) { // 省略 return connectHOC(selectorFactory, { // 省略 }) } }初見殺しにもほどがあります:(´ཀ`」 ∠):
整理しましょう。
- connect関数はcreateConnect関数により作られる(返される)
- 作られるconnect関数はconnectHOC呼び出し(デフォルトはconnectAdvanced)結果を返す
connectAdvanced
connect関数が呼ばれたときの動作を詳しく見ていく前にまずは概要をつかみましょう。というわけでデフォルトのconnectHOCであるconnectAdvancedを見てみます。こちらはcomponents/connectAdvanced.jsに書かれています。
connectAdvanced.js抜粋export default function connectAdvanced( selectorFactory, { // 省略 } = {} ) { // 省略 return function wrapWithConnect(WrappedComponent) { // 省略 function ConnectFunction(props) { // 省略 const renderedWrappedComponent = useMemo( () => <WrappedComponent {...actualChildProps} ref={forwardedRef} />, [forwardedRef, WrappedComponent, actualChildProps] ) const renderedChild = useMemo(() => { if (shouldHandleStateChanges) { return ( <ContextToUse.Provider value={overriddenContextValue}> {renderedWrappedComponent} </ContextToUse.Provider> ) } return renderedWrappedComponent }, [ContextToUse, renderedWrappedComponent, overriddenContextValue]) return renderedChild ] const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction // 省略 return hoistStatics(Connect, WrappedComponent) } }関数内関数内関数で頭が痛くなりそうですが整理します。
- connectAdvancedはwrapWithConnect関数1を返す。これはconnect関数呼び出しの結果できるHOCである。
- wrapWithConnect関数は内部でConnectFunction関数を定義している。これはやっていることを見ると(JSXでレンダリングするまでの処理は長いが)普通のコンポーネントのようだ。
- つまり、HOC内で、HOCの引数(connectしたいコンポーネント)をレンダリングするコンポーネントを作って返している。これにより、元の(connectされていない)コンポーネントにmapStateToProps、mapDispatchToPropsで定義されたpropsが渡されるようになる。
ここまでがconnect関数およびconnectAdvancedコンポーネントの概要です。では詳細に踏み込みましょう。
・・・、connect関数を作るcreateConnectを先頭から読んでみるといきなりselectorだとかproxyだとか出てくるので「どう動くか(どう呼び出されるのか)」がよくわかりません。そこで、先頭から読むのではなく実際にコンポーネントのレンダリングを行っているConnectFunction関数から見ていくことにしましょう。ConnectFunction関数
useMemo(メモ化)の利用
ConnectFunction関数を見ると冒頭のコードは以下のようになっています。
connectAdvanced.js抜粋function ConnectFunction(props) { const [propsContext, forwardedRef, wrapperProps] = useMemo(() => { // Distinguish between actual "data" props that were passed to the wrapper component, // and values needed to control behavior (forwarded refs, alternate context instances). // To maintain the wrapperProps object reference, memoize this destructuring. const { forwardedRef, ...wrapperProps } = props return [props.context, forwardedRef, wrapperProps] }, [props])useMemoはReactが提供する関数で「第2引数の配列要素のいずれかが変化した場合のみ第1引数の処理(計算)をやり直す」というもののようです。
このような処理をメモ化と言います。ある処理(計算)がある引数のみに依存している場合(ここ超重要です)、引数の値が変わらなければ結果は変わりません。つまり、再計算を行うのは無駄になります。これを避けるのがメモ化と呼ばれるテクニックです。文章が重複してしまいますが、「引数が変わらないのであれば前に計算した値を使う。変わったのであれば再計算を行う」ということが行われます。個人的にはメモ化してる内容がそんなに重い処理には思いませんが。ともかくこのような最適化の取り組みが入っていると読みにくいので逆最適化すると以下のようになります。
なお単にコピペしただけでこのコードは実際には動かないのであしからず(同じ名前の定数2回宣言してるので文法エラーになります)const { forwardedRef, ...wrapperProps } = props const [propsContext, forwardedRef, wrapperProps] = [props.context, forwardedRef, wrapperProps]Context
ConnectFunction関数では上記のようにメモ化を使いつつ徐々にコンポーネント(ReduxのStateを使ってレンダリングするコンポーネント)のためのデータを用意しています。
その中でまず大事なのはContextです。Contextはpropsでも指定できるようですがまあ普通はその名の通り「コンテキスト(現在の文脈)」を使うでしょう。つまり三項演算子のelseの方のContext
が使われます。connectAdvanced.jsを改変const ContextToUse = propsContext && propsContext.Consumer && isContextConsumer(<propsContext.Consumer />) ? propsContext : Contextこの
Context
が何者なのかを調べるためにConnectFunction関数の外側にさかのぼります。すると以下のコードが見つかります。
つまり特に指定をしなければReactReduxContextが使われます。connectAdvanced.js抜粋export default function connectAdvanced( selectorFactory, // options object: { // 省略 // the context consumer to use context = ReactReduxContext, } = {} ) { // 省略 const Context = contextReactReduxContextはcomponents/Context.jsで定義されています。
Context.js抜粋export const ReactReduxContext = /*#__PURE__*/ React.createContext(null)上記のようにコンテキストはReactの機能です。ドキュメントはコンテクストになってるな。コンテクストの方が一般的?まあこの記事ではコンテキストで行きます。
Reactのドキュメントにあるようにデータはprops(引数)で渡すべきです。グローバル変数駄目絶対です。
しかし原則はわかるが階層が深くなると「書くのがめんどくさい」「間のコンポーネントで渡し忘れる」「途中に自作でないコンポーネントが挟まれててデータを流してくれない」などなど様々な問題があります。
これを解決してくれるのがコンテキストです。コンテキストは「グローバル変数のようなもの」ですが、大きく異なる点として「特定の状況のみでグローバル」となります。「特定の状況」というものが具体的になんなのかはそれこそ「場合(どのようなところで使われるプログラムなのか)による」ということになりますが2、Reactについていえば「あるコンポーネントの子孫」となります。Providerコンポーネント
ところでreact-reduxを使う場合はProviderコンポーネントでStoreを指定するのがお決まりです。
ReactDOM.render( <Provider store={store}> <App /> </Provider>, rootElement )予測はつくと思いますがProviderコンポーネントではコンテキストを使って子孫のコンポーネントにStoreを受け渡すようになっています。subscriptionについては更新時処理編で見ていきます。
Provider.js抜粋function Provider({ store, context, children }) { const contextValue = useMemo(() => { const subscription = new Subscription(store) subscription.onStateChange = subscription.notifyNestedSubs return { store, subscription } }, [store]) // 省略 const Context = context || ReactReduxContext return <Context.Provider value={contextValue}>{children}</Context.Provider> }Selector
話をConneectFunction関数に戻しましょう。上記のようにコンテキストに設定されたStoreが取り出されます。実際にはStoreがpropsで渡されてるか確認されていますがさくっと省略。
connectAdvanced.jsを改変const contextValue = useContext(ContextToUse) const store = contextValue.store次にStoreを使ってSelectorが作られています。はい出てきました謎の単語Selector。
connectAdvanced.js抜粋const childPropsSelector = useMemo(() => { // The child props selector needs the store reference as an input. // Re-create this selector whenever the store changes. return createChildSelector(store) }, [store])createChildSelectorはConnectFunction関数の親(関数内関数を定義している関数)のwrapWithConnect関数で定義されています。
wrapWithConnect関数とはconnect関数が返す関数の実体でした(関数と書きすぎてややこしい)connectAdvanced.js抜粋return function wrapWithConnect(WrappedComponent) { // 省略 const selectorFactoryOptions = { ...connectOptions, getDisplayName, methodName, renderCountProp, shouldHandleStateChanges, storeKey, displayName, wrappedComponentName, WrappedComponent } function createChildSelector(store) { return selectorFactory(store.dispatch, selectorFactoryOptions) }selectorFactory
コードを見ていく前にとりあえずファクトリというものについての一般知識を。
「あるインターフェース」に対して複数の実装がある場合、「インターフェースを実装するオブジェクトの作成処理」を分けておけば(作成を行うオブジェクトを用意しておけば)実装を切り替えやすくなります。これがファクトリと呼ばれるパターンです。これからいろいろなファクトリが出てきますが、react-reduxの場合、「渡された引数に対して呼び出し側が想定するオブジェクトを返す関数」をファクトリを使って作成しています。
さてselectorFactoryとして渡される関数はconnect/selectorFactory.jsに定義されていますが、はっきり言って難解です。
selectorFactory.js抜粋export default function finalPropsSelectorFactory( dispatch, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options } ) { const mapStateToProps = initMapStateToProps(dispatch, options) const mapDispatchToProps = initMapDispatchToProps(dispatch, options) const mergeProps = initMergeProps(dispatch, options) const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory return selectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, options ) }雰囲気はわかるものの、とりあえずここまでで引数がどう渡されてきたのかを振り返ってみましょう。
- createConnect関数
connect関数に渡されるmapStateToProps等を使ってinitMapStateToProps等を定義。connectAdvancedにオブジェクトとして渡す3。- connectAdvanced関数
initMapStateToProps等は分割代入のその他(レストパラメータ)であるconnectOptionsに格納される。それがそのままselectorFactoryOptionsに渡される。- finalPropsSelectorFactory関数
分割代入を使って取り出す。このような形でfinalPropsSelectorFactoryにmapStateToProps等が渡されてきます。自分が使うパラメータのみ取り出し、残りはその他大勢として下位の関数に渡すというのは便利な反面、「なんか取り出してるけど、これどこで設定されたものだっけ」ということにもなるなと思いました。
initMapStateToProps
次にinitMapStateToPropsについて見ていきましょう。今まで読み飛ばしていた部分に目を向ける必要があります。
connect.js抜粋export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) { return function connect( mapStateToProps, mapDispatchToProps, mergeProps, { // 省略 } = {} ) { const initMapStateToProps = match( mapStateToProps, mapStateToPropsFactories, 'mapStateToProps' ) // 省略 } } function match(arg, factories, name) { for (let i = factories.length - 1; i >= 0; i--) { const result = factories[i](arg) if (result) return result } return (dispatch, options) => { throw new Error( `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${ options.wrappedComponentName }.` ) } }defaultMapStateToPropsFactoriesはconnect/mapStateToProps.jsに定義されています。mapDispatchToProps.jsの方がもう少しおもしろいのですが「connect関数に渡された引数をチェックして後の処理のところで場合分けしなくて済むように適切な関数」が設定されるようになっています。
mapStateToProps.js抜粋export function whenMapStateToPropsIsFunction(mapStateToProps) { return typeof mapStateToProps === 'function' ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps') : undefined } export function whenMapStateToPropsIsMissing(mapStateToProps) { return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined } export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]さて、wrapMapToPropsFuncはconnect/wrapMapToProps.jsに書かれています。
wrapMapToProps.js抜粋export function wrapMapToPropsFunc(mapToProps, methodName) { return function initProxySelector(dispatch, { displayName }) { const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) { return proxy.dependsOnOwnProps ? proxy.mapToProps(stateOrDispatch, ownProps) : proxy.mapToProps(stateOrDispatch) } // allow detectFactoryAndVerify to get ownProps proxy.dependsOnOwnProps = true proxy.mapToProps = function detectFactoryAndVerify( stateOrDispatch, ownProps ) { proxy.mapToProps = mapToProps proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps) let props = proxy(stateOrDispatch, ownProps) // 省略 return props } return proxy } }何これ(。´・ω・)?
このような関数内関数内関数に出くわしたときはconnectAdvanced概観のときにも示したように各関数がいつ呼ばれるのか整理しましょう。
- wrapMapToPropsFunc
connect関数の初めに呼び出される。initProxySelectorを返す(これがinitMapStateToPropsに代入される)- initProxySelector
finalPropsSelectorFactory関数の初めに呼び出される。mapToPropsProxy関数を返す(これがfinalPropsSelectorFactory関数でのmapStateToPropsに代入される)- mapToPropsProxy
今まで見てきたところではまだ呼ばれていない。というわけでmapToPropsProxy関数はまだ呼ばれていませんがmapStateToPropsに代入されることを考えるとconnect関数に自分が渡したmapStateToPropsを呼び出すこととほぼ同じような振る舞いをすると推測されます。ただしこのproxyがどう動いているのか、言語的な意味で難解です。
wrapMapToProps.js抜粋const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) { return proxy.dependsOnOwnProps ? proxy.mapToProps(stateOrDispatch, ownProps) : proxy.mapToProps(stateOrDispatch) } // allow detectFactoryAndVerify to get ownProps proxy.dependsOnOwnProps = true proxy.mapToProps = function detectFactoryAndVerify( stateOrDispatch, ownProps ) { proxy.mapToProps = mapToProps proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps) let props = proxy(stateOrDispatch, ownProps) // 省略 return props }特にここ
wrapMapToProps.js抜粋proxy.mapToProps = function detectFactoryAndVerify( stateOrDispatch, ownProps ) { proxy.mapToProps = mapToProps proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps) let props = proxy(stateOrDispatch, ownProps)答えとしては、このproxyは以下のように動きます。
- mapToPropsProxyが呼び出される。
- mapToPropsProxyのmapToPropsプロパティとして設定されているdetectFactoryAndVerifyが呼び出される。
- mapToPropsProxyのmapToPropsプロパティが自分が渡したmapStateToPropsに置き換えられる。
- mapToPropsProxyが呼び出される。
- mapToPropsProxyのmapToPropsプロパティが呼び出されるが今度は自分が渡したmapStateToPropsが呼び出される。
2回目以降は2~4の動作はなく自分が渡したmapStateToPropsが直接呼び出されます。
何故このようなややこしいことをしているかと言うと省略しているところで追加の処理をしているからなわけですが(これがproxyが存在する主な理由)、その処理が行われるのは一般的な(基本レベルでの)使い方ではないので初めに見るときはさくっと読み飛ばしてしまうのがいいと思います。また整理のところで書いたように「代入される変数名からするとこれはこういう動作をするのだろう」と推測するのもいいと思います。initMapStateToPropsの中身を見ていくのがだいぶ長くなってしまいましたが、finalPropsSelectorFactory関数に戻るとpureかどうか(デフォルトはtrue)に応じてfactoryを切り替え、selectorを作成しています。selectorとはまたしても関数です。
というわけでselectorの中身に踏み込むのは呼ばれるところでやることにしてConnectFunction関数に戻りましょう。actualChildPropsの作成
selectorを作成後、subscriptionの設定が行われていますがsubscriptionについては更新時処理編後で見るので読み飛ばします。
とすると結局以下のactualChildPropsを作っているところまで進みます。usePureOnlyMemoはpureがtrueの場合はuseMemoと同様です(falseの場合は常に第1引数の関数が実行されます)connectAdvanced.js抜粋const actualChildProps = usePureOnlyMemo(() => { return childPropsSelector(store.getState(), wrapperProps) }, [store, previousStateUpdateResult, wrapperProps])childPropsSelectorの実体はpureFinalPropsSelectorFactory関数が返す関数です(キーワード引数のpureがtrueの場合)。pureの方が処理がシンプルなのかと思ったらimpureの方がわかりやすいですね。というわけでimpureFinalPropsSelectorFactoryの方を見てみます。
selectorFactory.js抜粋export function impureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch ) { return function impureFinalPropsSelector(state, ownProps) { return mergeProps( mapStateToProps(state, ownProps), mapDispatchToProps(dispatch, ownProps), ownProps ) } }やっていることは簡単です。mapStateToPropsを呼び出して、mapDispatchToPropsを呼び出して、結果をマージしたpropsを返す。当たり前と言えば当たり前の操作が行われています。
pureFinalPropsSelectorFactoryはどうなっているかと言うと例に寄って初見ではどう動くのかわかりにくいのですが落ち着いて見ていけば読み解けます。ちょっと順番を入れ替えて示すと以下のようになります。
selectorFactory.jsを改変export function pureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, // ↓これらはconnectから渡されてきたもの { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } ) { // mapStateToProps等の呼び出し結果を保存しておくための変数 let hasRunAtLeastOnce = false let state let ownProps let stateProps let dispatchProps let mergedProps // 一回目に呼ばれる。mapStateToProps等の呼び出し結果をキャッシュしておく function handleFirstCall(firstState, firstOwnProps) { state = firstState ownProps = firstOwnProps stateProps = mapStateToProps(state, ownProps) dispatchProps = mapDispatchToProps(dispatch, ownProps) mergedProps = mergeProps(stateProps, dispatchProps, ownProps) hasRunAtLeastOnce = true return mergedProps } // 二回目以降に呼ばれる。変更がなければmapStateToProps等の呼び出しは行わない function handleSubsequentCalls(nextState, nextOwnProps) { const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps) const stateChanged = !areStatesEqual(nextState, state) state = nextState ownProps = nextOwnProps if (propsChanged && stateChanged) return handleNewPropsAndNewState() if (propsChanged) return handleNewProps() if (stateChanged) return handleNewState() return mergedProps } // Stateに変更があった場合に呼び出される // mapStateToPropsが返すオブジェクトが変わらなければマージし直さない // ただしオブジェクト比較はデフォルトではシャロ―比較 function handleNewState() { const nextStateProps = mapStateToProps(state, ownProps) const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps) stateProps = nextStateProps if (statePropsChanged) mergedProps = mergeProps(stateProps, dispatchProps, ownProps) return mergedProps } // これがchildPropsSelectorとして呼び出される関数 return function pureFinalPropsSelector(nextState, nextOwnProps) { return hasRunAtLeastOnce ? handleSubsequentCalls(nextState, nextOwnProps) : handleFirstCall(nextState, nextOwnProps) } }つまり、pureである(コンポーネントに指定されているpropsもしくはReduxのStateのみに依存する)場合は可能な限り余計な処理は行わないようになっています。
コンポーネントの描画
actualChildProps作成後、Store更新に対するsubscriptionの設定がされていますが読み飛ばします。とするとConnectFunction関数の残りは以下となります。
connectAdvanced.js抜粋// Now that all that's done, we can finally try to actually render the child component. // We memoize the elements for the rendered child component as an optimization. const renderedWrappedComponent = useMemo( () => <WrappedComponent {...actualChildProps} ref={forwardedRef} />, [forwardedRef, WrappedComponent, actualChildProps] ) // If React sees the exact same element reference as last time, it bails out of re-rendering // that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate. const renderedChild = useMemo(() => { if (shouldHandleStateChanges) { // If this component is subscribed to store updates, we need to pass its own // subscription instance down to our descendants. That means rendering the same // Context instance, and putting a different value into the context. return ( <ContextToUse.Provider value={overriddenContextValue}> {renderedWrappedComponent} </ContextToUse.Provider> ) } return renderedWrappedComponent }, [ContextToUse, renderedWrappedComponent, overriddenContextValue]) return renderedChild初めに確認したJSXにたどり着きました!これにて長かったConnectFunction関数は終了です!
なおshouldHandleStateChangesはmapStateToPropsが渡されてたらtrue、渡されてなかったらfalseです。ここまでのまとめ
まず言語によらないプログラミング技術としては以下のものがありました。4
- メモ化
- 時間のかかる処理について、引数が変わらなければ結果は変わらないとして処理結果をキャッシュする手法
- コンテキスト
- 「ある状況」においてグローバルな変数を定義する手法(引数を渡していく手間が省ける)
- ファクトリ
- 「あるインターフェース」に対する実装を作成するオブジェクトを用意し実装を切り替えやすくする手法
JavaScript的なプログラミング技術としてはキーワード引数、特に分割代入との組み合わせて渡された値を受け取るということが行われていました。
コードリーディングの観点では以下のような事項がありました。
- 関数内関数や関数内関数内関数について見る際にそれらがいつ呼ばれるのか整理する
- やけにややこしいことをしているところはあまり気にしない。「こう動くのだろう」という推測も大事
- 同じインターフェースの複雑な処理と簡単な処理があったらまず簡単な処理の方を見てインターフェースに対する理解を深めてから複雑な処理に挑む
react-reduxを読んでみようとしたきっかけとして、クロージャがどういう場合に使われるかの実例紹介という目的があったのですがこれは実例として紹介するのはやめておいた方がいいですね(笑)
さて、更新時処理に続きます。
- 投稿日:2020-03-14T17:11:46+09:00
『非同期でのメッセージ投稿』が理解できる最低限のRailsアプリを丁寧に作る(Ajax苦手の自分とお別れしよう)
この記事の基本的な方針
Ajaxはなんだか難しい!は、勘違いです。一歩一歩きちんと進んでいけば、普通のことだと思えてくるでしょう。
ここではAjax非同期通信を理解するためだけの簡易なアプリを一つ丁寧に作成して、Ajax学習の基礎を完了することを目的としています。この記事は、以下の「登録画面」「ログイン画面」「TOP画面」の3画面の簡単なアプリを元に拡張していきます
【TOP画面(ログイン前)】 【TOP画面(ログイン後)】
![]()
【登録画面】
【ログイン画面】
手を動かしながら読みたいようでしたら、以下でこの3画面アプリを手に入れてください。
Terminal$ git clone -b 超最低限のRailsアプリ(messageコントローラVer) https://github.com/annaPanda8170/minimum_rails_application.git $ bundle install $ bundle exec rake db:create $ bundle exec rake db:migrateこれ自体の作り方はこちら。
想定する読み手
既に一度Railsアプリをチュートリアルやスクール等で作ったことがある方であり、JQueryの基本文法を理解している方を想定しております。
Mac使用で、パソコンの環境構築は完了していることが前提です。取り急ぎ同期通信のメッセージ投稿機能をつくる
※本筋ではないので急ぎ足で説明します。詳しい説明が必要な方は、別記事をご覧ください(newアクションを使わず、formをindexに置くという違いがあります)。
① メッセージテーブルを作る
マイグレーションファイル
とモデル
を作るため、Terminal$ rails g model message
を打ちます。
マイグレーションファイルにdb/migrate/2020xxxxxxxxxx_create_messages.rbclass CreateMessages < ActiveRecord::Migration[5.2] def change create_table :messages do |t| t.string :message t.references :user, foreign_key: true t.timestamps end end endとなるように追記し、
Terminal$ rails db:migrate
します。
これを済ませたら、メッセージテーブルは完成です。一応ちゃんと出来ているかデータベースを見に行ってみましょう。
私はSequelProを使っていてこんな感じです。モデルにテーブル同士の関係を書きましょう。これが無くても投稿できなくはないのですが、投稿した内容を引き出して扱う上で便利なので今済ませてしまいましょう。以下を追記します。
app/models/message.rbbelongs_to :userapp/models/user.rbhas_many :messages②メッセージだけを投稿できるようにする
ルーティング、ビュー、コントローラを編集します。今回はindexにフォームを置くのでnewアクションはなしです。
config/routes.rbRails.application.routes.draw do devise_for :users root 'messages#index' resources :messages, only: [:index, :create] endapp/views/messages/index.html.erb<% if user_signed_in? %> <%= current_user.email %> <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %> <%= form_with(model: @message, local: true, class: "form") do |f| %> <%= f.text_field :message %> <%= f.submit "投稿" %> <% end %> <% else %> <%= link_to '新規登録', new_user_registration_path %> <%= link_to 'ログイン', new_user_session_path %> <% end %>この時点で、
localhost:3000
でもlocalhost:3000/messages
でも
が表示されるはずです。この時点ではこの投稿フォームただの飾りなので、データベースに保存できるよう中身を作ります。
コントローラは以下です。app/controllers/messages_controller.rbclass MessagesController < ApplicationController before_action :to_root, except: [:index] def index @message = Message.new end def create @message = Message.new(message_params) @message.save redirect_to root_path end private def message_params params.require(:message).permit(:message).merge(user_id: current_user.id) end def to_root redirect_to root_path unless user_signed_in? end endこれで一度投稿してみましょう。
③投稿を表示
あとはTOP画面に投稿されたものを全て表示させます。以下を追記します。
app/controllers/messages_controller.rbclass MessagesController < ApplicationController 〜省略〜 def index @messages = Message.all @message = Message.new end 〜省略〜 endapp/views/messages/new.html.erb〜省略〜 <% @messages.each do |m| %> <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %></span><%= m.message %></div> <% end %>投稿するときに画像の上の丸い矢印が一瞬
×
になって投稿が反映されると思いますが、これなしに反映されるのが非同期通信です。④JQueryを導入し、turbolinksを削除する
turbolinksを削除する理由は、JQueryの動きを阻害する可能性があるからです。(リロードすればjsが動作するのに、リンクで移動すると動作しない、等)
Gemfile
にgem 'jquery-rails'
を加え、gem 'turbolinks'
をコメントアウトするか消し、gemfile× gem 'turbolinks' gem 'jquery-rails'
bundle install
しサーバの再起動します。
app/assets/javascripts/application.js
に//= require jquery
と//= require jquery_ujs
を//= require_tree .
より上に追記し、//= require turbolinks
を消します。app/assets/javascripts/application.js× //= require turbolinks //= require jquery //= require jquery_ujs //= require_tree .以下を修正します。
app/views/layouts/application.html.erb`× <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> | v ◯ <%= stylesheet_link_tag 'application', media: 'all' %> <%= javascript_include_tag 'application' %>続いて
app/assets/javascripts
にmessages.js
を作ります。app/assets/javascripts/messages.coffee
があると作成したファイルが機能しないので削除します。app/assets/javascripts/messages.js$(function () { console.log("OK") });を書いて、ブラウザをどの画面でもいいのでリロードします。
コンソールにOKが表示されたら成功です。これで準備は終わりです。
いよいよ本筋、非同期実装
完成品GitHub(masterではなく一つのブランチなので注意して下さい)
①投稿ボタンを押すとイベント発火させる
以下のようにjsファイルを直しフォームの投稿ボタンが押されたときにコンソールに
Ok
が出てくるか確認します。
function後の()にeをお忘れなく。app/assets/javascripts/messages.js$(function (e) { $(".form").on("submit", function () { console.log("Ok") }) });一瞬だけ表示されてすぐに消えますね。投稿されたらTOP画面に(つまり同じ画面に)リダイレクトするのですから当然ですね。今はリダイレクトせずに投稿が反映されるようにするためにこの動きをjs内で止めます。以下に直してください。
app/assets/javascripts/messages.js$(function () { $(".form").on("submit", function (e) { console.log("Ok") e.preventDefault(); }) });これで
Ok
が残るようになりました。②formの情報をjsで受け取り、createアクションに渡して投稿する
まずjsでformの情報を受け取る型は以下のような感じです。
app/assets/javascripts/messages.js$(function () { $(".form").on("submit", function (e) { e.preventDefault(); $.ajax({ url: (1) , type: (2) , data: (3) , dataType: 'json', }) }) });加わったのは
$.ajax
のくだりですね。
4つの項目がありますが、dataType
はとりあえず'json'
でいいです。jsonとはデータの形式で、{a: b,c: d}
みたいなやつです。Rubyでいうハッシュ、JavaScriptでいくオブジェクトですね。簡単です。
(json以外にも、XMLやHTMLでもできるみたいですね)(1)〜(3)を埋めて行きます。
(1)はcreateアクションにいくurlです。rails route
で確認すれば一発ですね。今回の私の場合は/messages
です。
(2)は、HTTPメソッドです。createアクションに行くので、'POST'
ですね。
(3)は、検証ツールでメッセージを書き込むinputタグのname属性をみればわかります。
ありました。このように参照される値なので、{message: {message: <投稿内容> }}
のように渡せばいいですね。
では投稿内容はどうすれば良いかというとidがmessage_message
になっているので、$("#message_message").val()
で取れます。(詳しい説明は省きます)
これを埋めて、投稿完了したときにたどり着くdoneメソッドをajaxメソッドに連ねて書きます。app/assets/javascripts/messages.js$(function () { $(".form").on("submit", function (e) { e.preventDefault() $.ajax({ url: "/messages" , type: "POST" , data: {message: {message: $("#message_message").val() }} , dataType: 'json', }).done(function (data) { console.log("ok"); }); }) });これで一度投稿してみましょう。データベースを見れば投稿は成功しているのがみて取れます。
以下に同期・非同期の両方でのターミナルでの状態を掲載します。このような違いが出てますね。そしてなぜかコントローラのcreateアクションの最後の
redirect_to root_path
が効かなくなりました。今コンソールにok
は見られません。コントローラに残って機能しなくなったredirect_to root_path
が阻害しているようです。これを削除してもう一度投稿すれば、コンソールにok
が見られるはずです。このあと,投稿が完了した時に、formの値を全てなくして、投稿ボタンを蘇るようにします。
formの値をまとめてなくすには$('.form')[0].reset();
を追記します。[0]
がなぜ必要なのかはよくわかりません。
続いて投稿ボタンはerbファイルでボタンに適当にidを指定して(私は<%= f.submit "投稿", id:"bbb" %>
こうしました)、$('#bbb').prop('disabled', false);
を追記します。
全体を見てみます。app/assets/javascripts/messages.js$(function () { $(".form").on("submit", function (e) { e.preventDefault() $.ajax({ url: "/messages" , type: "POST" , data: {message: {message: $("#message_message").val() }} , dataType: 'json', }).done(function () { $('.form')[0].reset(); $('#bbb').prop('disabled', false); }); }) });これでリロードしなくでも、何回でも投稿できるようになりました。
あとは表示できるようにすればOKですね。
③投稿内容を非同期で表示させる
Ajaxでやってきたデータを扱うには
respond_to
メソッドを使います。createアクションの最後の
redirect_to root_path
があった場所に、以下を追記します。app/controllers/messages_controller.rbrespond_to do |format| format.json {render json: { ccc: @message.message , ddd: @message.user.email}} endメッセージ投稿内容とメッセージを送った人のEmailをそれぞれcccとdddに格納してjson形式でレンダーしますよってことですね。
あとは、doneイベント内で情報を受け取ってHTMLに整形してappendするだけです。
doneイベント内のfunction後の()
内に何か文字をおけばそこに上のjsonデータが格納されます。今回はeee
としてみました。これをコンソール出力してみます。app/assets/javascripts/messages.js〜省略〜 }).done(function (eee) { console.log(eee); $('.form')[0].reset(); $('#bbb').prop('disabled', false); }); 〜省略〜これで投稿してみると、コンソールで
Output{ccc: "ハロー", ddd: "aaa@aaa"}大丈夫そうですね。 これが
eee
の中に入っているわけですから、"ハロー"を取得するにはeee.ccc
で、"aaa@aaa"を取得するには…大丈夫ですね。あとは、これを表示させます。appendするために
app/views/messages/new.html.erb〜省略〜 <% @messages.each do |m| %> <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %></span><%= m.message %></div> <% end %>これをdivタグで囲って、適当なidをつけます。今回は
aaa
としました。app/views/messages/new.html.erb〜省略〜 <div id="aaa"> <% @messages.each do |m| %> <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %> </span><%= m.message %></div> <% end %> </div>あとは
app/assets/javascripts/messages.js$("#aaa").append(`<div style="margin-top: 20px;"><span style="color: red;">${eee.ddd}</span>${eee.ccc}</div>`)を追記するだけです。
これで完成です。投稿して確認してください。最後に再掲します。
app/assets/javascripts/messages.js$(function () { $(".form").on("submit", function (e) { e.preventDefault(); $.ajax({ url: "/messages", type: "POST", data: { message: { message: $("#message_message").val() } }, dataType: 'json', }).done(function (eee) { $('.form')[0].reset(); $('#bbb').prop('disabled', false); $("#aaa").append(`<div style="margin-top: 20px;"><span style="color: red;">${eee.ddd}</span>${eee.ccc}</div>`) }); }) });app/views/messages/new.html.erb<% if user_signed_in? %> <%= current_user.email %> <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %> <%= form_with(model: @message, local: true, class: "form") do |f| %> <%= f.text_field :message %> <%= f.submit "投稿" , id: "bbb"%> <% end %> <div id="aaa"> <% @messages.each do |m| %> <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %> </span><%= m.message %></div> <% end %> </div> <% else %> <%= link_to '新規登録', new_user_registration_path %> <%= link_to 'ログイン', new_user_session_path %> <% end %>まとめ
本当に最低限です。
これを、整えて十分な状態にするための続きをまた書きます。
フォローしてお待ち下さい。
- 投稿日:2020-03-14T16:59:05+09:00
jsの型まとめ
型
プリミティブ型とオブジェクト型に別れる。
値 属性 プリミティブ型 immutable プロパティやメソッドを持たない オブジェクト型 mutable プロパティやメソッドを持つ プリミティブ型 → いわゆる値渡し
オブジェクト型 → いわゆる参照渡し
(ただしこの認識は正確性を欠く。正しくは以下の記事をご参考に)
https://qiita.com/yuta0801/items/f8690a6e129c594de5fb文字列型と文字列オブジェクト(Stringクラス)
var str = "abc" //文字列型 var strObj = new String("abc") //文字列オブジェクト文字列型と文字列オブジェクトの暗黙裡の変換
本来、
str
にはメソッドがないのでlengthメソッドで長さを取得できないが、実際にはstr.length
として取得できる。これは、lengthメソッドへのアクセスの際に文字列オブジェクトへ暗黙の型変換が行われ、実行後元の文字列型に戻るため、見た目上(typeof演算子上)でstring
だが、一時的にオブジェクトとしての振る舞いができる。
str.length
や"abc".length
が可能なのは一時的にキャストされているため。なお、逆に、+演算子で文字列型と文字列オブジェクトをくっつける場合には、文字列型への暗黙の変換が行われる。
type(strObj) // 'object' type(strObj + "def") // 文字列型に変換される。 // 'string'なお、==演算子と===演算子は、比較時に暗黙的に型を変換するかの違い。
前者は変換を行うが、後者は行わないたね、より厳密な型チェックとなる。str == str // true str === str // true str == strObj // 型変換されるため // true str === strObj // false strObj == new String("abc") // new演算子で生成されるオブジェクトは中身が同じでも参照先が異なるため不一致 // false型変換関数 String()
String()は明示的な型キャストの関数。実はStringクラスのメソッド。
pythonでいうところのstr()
、C++でいうところのstatic_cast<String>
Stringクラスのプロパティ
よく使うようtoString()やmatch()、trim()などはいづれもprototupeプロパティの中にある。lengthはこの中ではなく、prototypeと並列に位置しており、常に値は1である。
なお、プロパティに持つ関数は全て非破壊的な関数であり、文字列を書き換えることはせず新しい値を返すものになる。以下のようにプロパティを用いた場合、新しい文字列が生成され、もとのオブジェクトは不変(immutable)である。
strObj.toUpperCase() // 'ABC' typeof(obj.toUpperCase()) // 'string'Stringオブジェクトのプロパティ
文字列値とlengthを持つ。このlengthは文字列長さを返す。
数値型と数値オブジェクト(Numberクラス)
64bitの浮動小数点型(これ以外にはない)。
var num = 1 //数値型 var numObj = new Number(1) //数値オブジェクト数値型と数値オブジェクトの暗黙裡の変換
文字列とStringクラスの関係と一緒。一時的なキャストにより
num.toString()
や(1).toString
が可能。括弧は小数点との区別のために必要。型変換関数 Number()
Number()は明示的な型キャストの関数。実はNumberクラスのメソッド。
Numberクラスのプロパティ
Stringクラスより多くのプロパティがある。
プロパティ名 意味 length 常に1 MAX_VALUE 正の最大値 MIN_VALUE 正の最小値 NaN Not a Number NEGATIVE_INFINITY -∞ POSITIVE_INFINITY +∞ prototype プロトタイプ 特別値(NaN、±INFINITY)
NaNを与えた全ての演算の結果はNaNとなる。これは比較演算子も例外ではないため、NaNであることの確認には定義済み関数であるisNaN()を用いる必要がある。
同じくisFinite()関数ではinfinityおよびNaN以外であるかの判定が可能である。
また、特別値との剰余算の結果は以下。(正常値としては1を取り上げる)
x y x/y x%y 1 ∞ 0 1 1 0 ∞ NaN 0 0 NaN NaN ∞ 1 ∞ NaN ∞ ∞ NaN NaN 真偽値型と真偽値オブジェクト(Booleanクラス)
ほんとか嘘か。trueかfalse。
var flag = true //数値型 var flag = new Boolean(true) //数値オブジェクト真偽値型と真偽値オブジェクトの暗黙裡の変換
文字列とStringクラスの関係と一緒。一時的なキャストにより
num.toString()
や(1).toString
が可能。型変換関数 Boolean()
Boolean()は明示的な型キャストの関数。実はBooleanクラスのメソッド。
通常、読み取り時に暗黙裡に型変換されるので明示的に変換する必要はない。Booleanクラスのプロパティ
prototypeとlength(常に1)のみ。なお、Booleanオブジェクトにはlengthプロパティがない。
null型
何も参照していない状態を意味する。null型はnull値のみとることができ、nullはリテラル値である。
ただし、typeof演算子にかけると返値はobjectであるため===演算で確認する必要がある。typeof(null) // 'object' null === null trueまたnullにはNullクラスが存在しないため、プロパティやメソッドを有さない。
undefined型
同じくundefined値のみ持つ。typeof演算の結果は文字列undefinedが返る。nullと違い、undefinedはリテラル値ではなく、定義されたグローバル変数である。
null同様にUndefinedクラスは存在しない。BigInt型、Symbol型
// これから書きます。
object型
上述の基本型以外は全てobjectである。
ただし、関数は、objectの一種であるもののtypeof演算の結果は'function'を返す。型変換
文字列 → 数値
明示的なキャスト
Number() 問答無用にnumber型に変える。数値以外が入ってるとNaNになる。 parseInt() 数値以外は無視して整数へ。第2引数に基数(n進数)を指定できる parseFloat() 数値以外は無視して少数へ。 parseInt("ff", 16) // 255暗黙的なキャスト
//単項演算子(+) +'1' // 1 //算術演算子(-) '2' - '1' // 1ただし、算術演算子(+)は、文字列の結合を可能なため、上記の演算を行うと21が返る。
数値 → 文字列
明示的なキャスト
String(1) (1).toString()暗黙的なキャスト
//算術演算子(+) '2' + '1' // 21その他の型変換
null型 → 数値型
null型nullを数値型へ変換すると0になる。NaNではない!
object型
変換先 実行されるメソッド 文字列型 toString 数値型 valueOf(もしくはtoString)なければ'NaN' 真偽値型 常にtrue よくデバッグで
console.log("value:" + obj)
とすると[object Object]
が入ってて中に潜れなくなるのはこのため。
- 投稿日:2020-03-14T16:08:50+09:00
js→railsへのajax通信で404エラーを出しまくった話。
はじめに
はい皆の衆よくお聞き
初心者様による
初心者でもできるエラー殺し講座を始めるよく聞かないと死にます
前回、ancestryとjQueryで多階層型カテゴリの入力フォームを段階的に表示させてみたを投稿するためにサンプルを作ったのですが、さすが初心者。
サンプルを作り終える間に多大なエラーを出しては修正し、修正しては出ししていました。
今回はそのエラーから「404 NotFound」を切り出して、どのように解決していったかを記事にしていきます。今回取り扱うエラーについて
404 (Not Found)
HTTP 404、またはエラーメッセージ Not Found(「未検出」「見つかりません」の意)は、HTTPステータスコードの一つ。 クライアントがサーバに接続できたものの、クライアントの要求に該当するもの (ウェブページ等) をサーバが見つけられなかったことを示すもの。(wikipediaより抜粋)第一の404 (Not Found)
まず、「js書くべし」で以下のjsを書きました。
assets/javascript/items.js$(function() { function buildHTML(result){ var html = `<option value= ${result.id}>${result.name}</option>` return html } $("#parent").on("change",function(){ var int = document.getElementById("parent").value if(int == 0){ $('#child').remove(); $('#item_category_id').remove(); }else{ $.ajax({ url: "categories/", type: 'GET', dataType: 'json', data: {id: int} }) .done(function(categories) { var insertHTML = `<select name="child" id="child"> <option value=0>---</option>`; $.each(categories, function(i, category) { insertHTML += buildHTML(category) }); insertHTML += `</select>` if($('#child').length){ $('#child').replaceWith(insertHTML); $('#item_category_id').remove(); } else { $('.items__child').append(insertHTML); }; }) .fail(function() { }); }; }); })はい、もーなんか既に分かる人には分かりますね。
原因
原因はこれでした。
url: "categories/",
以下のように変更しました。
url: "categories/",
=>url: "/categories",
気付いたきっかけ
ターミナルを見た時に以下のようになっていました。
Started GET "/items/categories/?id=1" for ::1 at 2020-03-14 15:17:23 +0900
ActionController::RoutingError (No route matches [GET] "/items/categories"):?なんでitemにネストしたurlになってるの?
ということで、url修正しました。第二の404 (Not Found)
今度のターミナルエラーはこれ。
Started GET "/categories?id=1" for ::1 at 2020-03-14 15:36:36 +0900
ActionController::RoutingError (No route matches [GET] "/categories"):原因
config/routes.rbRails.application.routes.draw do root "items#new" resources :items ,only: [:index,:new,:create] endただのroutingの設定忘れでした。
config/routes.rbRails.application.routes.draw do root "items#new" resources :items ,only: [:index,:new,:create] resources :categories ,only: :index end修正します。
第三の404 (Not Found)
ターミナルエラーはこれ。
Started GET "/categories?id=1" for ::1 at 2020-03-14 15:45:21 +0900
ActionController::RoutingError (uninitialized constant CategoriesController):原因
categories_controller.rbが無い・・・だと?
ちゃんとあるじゃ・・・ある・・・あ?
/controllers/category_controller.rbclass CategoryController < ApplicationController def index @categories = Category.where(ancestry: params[:id]) respond_to do |format| format.json end end end・・・controller作る時に単数形にしてますね・・・。
消して作り直します。terminal.$ rails d controller category $ rails g controller categories/controllers/categories_controller.rbclass CategoriesController < ApplicationController def index @categories = Category.where(ancestry: params[:id]) respond_to do |format| format.json end end end結果
今回の教訓
①エラーが出たらターミナルをよく見よう。
②routingはちゃんとやっておこう。
③controllerは複数形で作ろう。今回は以上です。
- 投稿日:2020-03-14T15:58:24+09:00
gRPC-Web + React Hooks + Go でリアルタイムチャットをつくる
概要
分散システムを学ぶうちにgRPCに興味を持った。きくところによると、gRPC-Webというものもあるらしい。
この記事では、gRPC-Web + React Hooks + Goを用いてリアルタイムチャットを作りながら、実装の流れを書いてみようと思う。
コードだけ見たいという方は↓へ、
gRPC-Webってなんやねん?という方は↓へどうぞ!
全体像
サービスの全体像は以下のようになる。
タイトルのとおり、ReactクライアントからgRPC-WebでGoサーバーと通信するチャットサービスだ。
デモはこんな感じ↓
(リアルタイムですべてのクライアントにメッセージが配信される)開発
Protocol Buffersの定義
まずはProtocol Buffersのインターフェイスを定義する。
前述のチャットサービスをつくるにあたって、以下のようなインターフェイスを作成する。
syntax = "proto3"; import "google/protobuf/empty.proto"; package messenger; service Messenger { rpc GetMessages (google.protobuf.Empty) returns (stream MessageResponse) {} rpc CreateMessage (MessageRequest) returns (MessageResponse) {} } message MessageRequest { string message = 1; } message MessageResponse { string message = 1; }
CreateMessage
はメッセージの投稿で、リクエストとレスポンスの型を定義している。GetMessages
でメッセージの受信をする。returns (stream MessageResponse)
とすることでストリームを返すコードを生成できる。gRPCのコードを自動生成
ここからgRPCのコードを生成する。
GoバックエンドとTypeScriptフロントエンドのコードを生成するために、
protoc
に加え、protoc-gen-go
、protoc-gen-grpc-web
をインストールする。もちろんローカル環境には入れたくないのでコンテナをつくっていく。
FROM golang:1.14.0 ENV DEBIAN_FRONTEND=noninteractive ARG PROTO_VERSION=3.11.4 ARG GRPCWEB_VERSION=1.0.7 WORKDIR /proto RUN apt-get -qq update && apt-get -qq install -y \ unzip RUN curl -sSL https://github.com/protocolbuffers/protobuf/releases/download/v${PROTO_VERSION}/\ protoc-${PROTO_VERSION}-linux-x86_64.zip -o protoc.zip && \ unzip -qq protoc.zip && \ cp ./bin/protoc /usr/local/bin/protoc && \ cp -r ./include /usr/local RUN curl -sSL https://github.com/grpc/grpc-web/releases/download/${GRPCWEB_VERSION}/\ protoc-gen-grpc-web-${GRPCWEB_VERSION}-linux-x86_64 -o /usr/local/bin/protoc-gen-grpc-web && \ chmod +x /usr/local/bin/protoc-gen-grpc-web RUN go get -u github.com/golang/protobuf/protoc-gen-godocker-compose.ymlversion: '3' services: proto: command: ./proto/scripts/protoc.sh build: context: . dockerfile: DockerfileProto volumes: - .:/protoprotoc.sh#!/bin/sh set -xe SERVER_OUTPUT_DIR=server/messenger CLIENT_OUTPUT_DIR=client/src/messenger protoc --version protoc --proto_path=proto messenger.proto \ --go_out=plugins="grpc:${SERVER_OUTPUT_DIR}" \ --js_out=import_style=commonjs:${CLIENT_OUTPUT_DIR} \ --grpc-web_out=import_style=typescript,mode=grpcwebtext:${CLIENT_OUTPUT_DIR}これで
docker-compose up
するとコードが自動生成される。バックエンドの実装
バックエンドの実装をする。
ひとつ前のステップで、以下のようなインターフェイスが自動生成されているので、これを組み合わせて実装をしてゆく。
// MessengerServer is the server API for Messenger service. type MessengerServer interface { GetMessages(*empty.Empty, Messenger_GetMessagesServer) error CreateMessage(context.Context, *MessageRequest) (*MessageResponse, error) }まずはサーバーの雛形を書いてみる。下記の
TODO
を埋めていくような流れだ。package main import ( "context" "log" "net" "github.com/golang/protobuf/ptypes/empty" pb "github.com/okmttdhr/grpc-web-react-hooks/messenger" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) const ( port = ":9090" ) type server struct { pb.UnimplementedMessengerServer requests []*pb.MessageRequest } func (s *server) GetMessages(_ *empty.Empty, stream pb.Messenger_GetMessagesServer) error { // TODO: 実装 } func (s *server) CreateMessage(ctx context.Context, r *pb.MessageRequest) (*pb.MessageResponse, error) { // TODO: 実装 } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterMessengerServer(s, &server{}) reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }まずはメッセージの投稿だが、シンプルに、配列に時刻付きのメッセージを詰め込んでゆく形にした。
func (s *server) CreateMessage(ctx context.Context, r *pb.MessageRequest) (*pb.MessageResponse, error) { log.Printf("Received: %v", r.GetMessage()) newR := &pb.MessageRequest{Message: r.GetMessage() + ": " + time.Now().Format("2006-01-02 15:04:05")} s.requests = append(s.requests, newR) return &pb.MessageResponse{Message: r.GetMessage()}, nil }次にメッセージの取得だ。一度目のアクセスで保持しているメッセージを流し、それ以降は、新しいメッセージを検知したときのみデータを送るようにしている。
func (s *server) GetMessages(_ *empty.Empty, stream pb.Messenger_GetMessagesServer) error { for _, r := range s.requests { if err := stream.Send(&pb.MessageResponse{Message: r.GetMessage()}); err != nil { return err } } previousCount := len(s.requests) for { currentCount := len(s.requests) if previousCount < currentCount { r := s.requests[currentCount-1] log.Printf("Sent: %v", r.GetMessage()) if err := stream.Send(&pb.MessageResponse{Message: r.GetMessage()}); err != nil { return err } } previousCount = currentCount } }これでバックエンドの実装ができた。
フロントエンドの実装
次に、Web側の実装を行う。
まずはgRPCと通信を行うためのクライアントをつくる。
MessengerClient
が自動生成されているので、以下のように使うことができる。(messenger/*
が自動生成)。import { MessengerClient } from "messenger/MessengerServiceClientPb"; export type GRPCClients = { messengerClient: MessengerClient; }; export const gRPCClients = { messengerClient: new MessengerClient(`http://localhost:8080`) };これを以下のように使うと、メッセージの受信ができる。
import { Empty } from "google-protobuf/google/protobuf/empty_pb"; const stream$ = client.getMessages(new Empty()); // イベントは`data`以外にも、`error`、`status`、`end`が生成される。 stream$.on("data", m => { console.log(m) });実際はhooksの中で使うので、以下のようなコードとなる。
import { Empty } from "google-protobuf/google/protobuf/empty_pb"; import { useState, useEffect } from "react"; import { MessengerClient } from "messenger/MessengerServiceClientPb"; export const useMessages = (client: MessengerClient) => { const [messages, setMessages] = useState<string[]>([]); useEffect(() => { const stream$ = client.getMessages(new Empty()); stream$.on("data", m => { setMessages(state => [...state, m.getMessage()]); }); }, [client]); return { messages }; };
messages
をstateとして持ち、ストリームからデータを受信するたびにmessages
を更新している。これを表示するコンポーネントは以下のようになる。
import React from "react"; type Props = { messages: string[]; }; export const Messages: React.FC<Props> = ({ messages }) => { return ( <div> {messages.map(m => ( <div key={m}>{m}</div> ))} </div> ); };コンポーネントからは
messages
だけを見ることで、gRPCのロジックを切り離すことができる。(hooksを呼び出す箇所は後述)。メッセージの投稿は以下のようにgRPCのコードを利用できる。
import { MessageRequest } from "messenger/messenger_pb"; const req = new MessageRequest(); req.setMessage(message); client.createMessage(req, null, res => console.log(res));同じようにhooksで使ってゆく。
import { MessageRequest } from "messenger/messenger_pb"; import { useState, useCallback, SyntheticEvent } from "react"; import { MessengerClient } from "messenger/MessengerServiceClientPb"; export const useMessageForm = (client: MessengerClient) => { const [message, setMessage] = useState<string>(""); // メッセージ入力欄 const onChange = useCallback( (event: SyntheticEvent) => { const target = event.target as HTMLInputElement; setMessage(target.value); }, [setMessage] ); // メッセージ投稿 const onSubmit = useCallback( (event: SyntheticEvent) => { event.preventDefault(); const req = new MessageRequest(); req.setMessage(message); client.createMessage(req, null, res => console.log(res)); setMessage(""); }, [client, message] ); return { message, onChange, onSubmit }; };フォームのコンポーネント
import React from "react"; import { useMessageForm } from "containers/Messages/hooks/useMessageForm"; type Props = ReturnType<typeof useMessageForm>; export const MessageForm: React.FC<Props> = ({ message, onChange, onSubmit }) => { return ( <form onSubmit={onSubmit}> <input type="text" value={message} onChange={onChange} /> </form> ); };hooksを使う側は以下のようになる。
import React from "react"; import { Messages } from "components/Messages"; import { MessageForm } from "components/MessageForm"; import { GRPCClients } from "gRPCClients"; import { useMessages } from "./hooks/useMessages"; import { useMessageForm } from "./hooks/useMessageForm"; type Props = { clients: GRPCClients; }; export const MessagesContainer: React.FC<Props> = ({ clients }) => { const messengerClient = clients.messengerClient; const messagesState = useMessages(messengerClient); const messageFormState = useMessageForm(messengerClient); return ( <div> <MessageForm {...messageFormState} /> <Messages {...messagesState} /> </div> ); };プロキシの設定
現時点でgRPC-Webを使うには、プロトコル間の微調整を行うためのプロキシが必要で、公式ではEnvoyを推奨していたりする。
Dockerイメージがいい感じに用意されているので、フロントエンドからはプロキシにリクエスト、プロキシコンテナはサーバーコンテナにlinkするだけである。
詳しく見たい方は以下へどうぞ。
これで一通りの実装が完了し、
docker-compose up
でアプリケーションが起動できるようになった。コードの全貌はGitHubに。
おわりに
gRPC-Web + React Hooks + Goを用いてリアルタイムチャットを作成してみた。
まだ制限もあるが、少なくともRESTの置き換えとしては十分候補に入れてよいのではないだろうか。また、領域を問わずコンテナベースでの開発がスタンダードになっていることを改めて実感できた。
参考
- 投稿日:2020-03-14T15:33:41+09:00
【大量のGmailを一括下書き作成】スプレッドシートから管理するスクリプトを書いたので使ってほしい。
Gmailで大量の送信先にメールを送るときにスプレッドシートで管理するスクリプトを書いたので使ってほしい。
スクショはこんな感じ。
下書き作成
ぼたんポチするとGmail側に下書きが保存されます。目で目視してから使ってくれ。
FAQ
- BCCじゃだめなんですか?
- Toでたくさん贈りたいときもあるよね…??
- え、もしかして ひとつひとつの宛先ごとにメール作ってくれるの?
- そうです!
- プログラムから実行すると、間違ったときに不安!!!
- 大丈夫です。下書き
GmailApp.createDraft(to, subject, body)
で作ってます!- 自分の目で確認してから、Gmailから送信ボタンぽちぽちしてください。
- 悪いことに使っていい?
- 絶対にやめてください。特定電子メール法に則って使ってください。
共有
ファイルを共有したので自分のドライブにコピーして使ってほしい。
https://docs.google.com/spreadsheets/d/1fPE17OVUx0-OBXCRTZAm-vkLn1msYerBis7rN1tMaRc/edit?usp=sharing
あ、もちろん実際に使われているコードがどんなのか見てから使ってほしいのと、Gmailから権限要求されるので適切に許可してあげてほしい。
実装
/** ***************************************** [ [ '項目', '内容', 'To', 'Name' ], [ '件名', 'XXXXの件について', 'example@example.com', '開発室example さま' ], [ '書き出し', '', 'example+123@example.com', '総務 田中 さま' ], [ '本文', '', '', '' ], [ 'To欄の数', 2, '', '' ] ] ***************************************** */ const allData = SpreadsheetApp.getActiveSheet().getDataRange().getValues(); /** ***************************************** ['XXXXの件について', '本文'] ***************************************** */ const commonData = allData.filter(x => (x[0] === "件名" || x[0] === "本文")).map(x => x[1]); /** ***************************************** [ [ 'tanaka@example.com', '開発室example さま' ], [ 'AAA@example.com', '総務 田中 さま' ] ] ***************************************** */ const toMailData = allData.filter(x => (x[2] !== "To" && x[2] !== "")).map(x => [x[2], x[3]]) function createDraft() { const subject = commonData[0]; // GmailApp.createDraft(["hirao@openlogi.com", "hirao+2@openlogi.com"], subject, "body test"); toMailData.forEach(async x => { console.log(x[0]); const to = x[0]; const body = createBody(x[1]); await GmailApp.createDraft(to, subject, body); }) } /** * @todo メッセージ本文入れたり署名欄とか拡張する */ function createBody(name) { return name + "\n\n" + commonData[1]; }
下書き作成
ってボタンをスプレッドシート内のGASの関数createDraft
に紐づけています。発火したら、今ひらいているページのデータを読み込みにいってメール送信するので、こんな感じでどんどんタブを追加していって、どの時はどの人たちにメールを送ったのか、そういう履歴もわかりますです。
以上。
今日さくっと作ったやつですが、必要な方いたら使ってください。
- 投稿日:2020-03-14T15:18:25+09:00
3分でわかるgRPC-Web
gRPCとは
gRPCの概要を簡単にまとめる。
- HTTP/2による高速な通信
- IDL(Protocol Buffers)でデータ構造とRPCを定義
- 多言語対応のソースコード自動生成
- Streamingを用いた双方向通信が可能
詳細は以下へ。
gRPC-Webとは
gRPC-WebによってgRPC通信をWebでも使うことができる。以上!
といえればいいのだが、実際は、ブラウザの制限にあわせたプロトコルを定義している。
そのため、現時点だと、プロトコル間の微調整を行うためのプロキシが必要で、公式ではEnvoyを推奨していたりする。
ブラウザの制限
前述したブラウザの制限とは、例えば以下のようなものだ。
- HTTP/2のフレーミングレイヤーはブラウザに露出されない
- ブラウザからのStreamingがまだ不十分 (WHATWG Streams)
- クライアントにHTTP/2の使用を強制できない
- クロスブラウザで動くbase64のようなテキストエンコーディングの必要性
上記により、以下のようなことがgRPC-Webでは不可能である。
- gRPCでサーバーとの「直接」通信 (Proxyを用意する必要がある)
- Client-side & Bi-directional streaming
少なくともBi-directional streamingがでできるようになればgRPC-Webの立場はかなり上がると思うので残念だ。
メリット
現時点でのgRPC-Webのメリットは以下のようなものがある。
- クライアントからサーバーまで、一気通貫でgRPCの開発パイプラインに載せられる
- バックエンド・フロントエンド間でタイトな連携ができる
- クライアント向けの「gRPCライブラリ」を容易に生成できる
例えば、バックエンドのサービス群がgRPCで構築されている時、HTTPのレイヤーでBFFを用意する必要がなくなり、不要なAPI設計やコミュニケーションをへらすことができるのがメリットになりそうだ。
下の2つは、IDLベースなこととコードの自動生成により、RESTなどで「仕様書ベース」で合意を行うよりも、スムーズな開発ができるということだと理解した。アプローチや思想は異なるが、GraphQLとも一部ゴールを共有しそうだ。
gRPC APIの設計
API設計のガイドラインをGoogleが用意していたりする。
使ってみた
実際にgRPC-Webをつかって簡単なチャットを作ってみた↓
参考
- 投稿日:2020-03-14T14:30:09+09:00
ハンバーガーメニュー
はじめに
サイトのレスポンシブに伴ってハンバーガーメニューを実装してみました。
コード
$(function(){ $(".btn-gnavi").on("click", function(){ var rightVal = 0; if($(this).hasClass("open")) { rightVal = -300; $(this).removeClass("open"); } else { $(this).addClass("open"); } $("#global-navi").stop().animate({ right: rightVal }, 200); }); });JavaScript
.btn-gnavi { position: fixed; top: 3vw; right: 4vw; width: 30px; height: 24px; z-index: 3; box-sizing: border-box; cursor: pointer; -webkit-transition: all 400ms; transition: all 400ms; } .btn-gnavi span { position: absolute; width: 30px; height: 4px; background: black; border-radius: 10px; -webkit-transition: all 400ms; transition: all 400ms } .btn-gnavi span:nth-child(1) { top: 0; margin-left: 5px; } .btn-gnavi span:nth-child(2) { top: 10px; margin-left: 5px; } .btn-gnavi span:nth-child(3) { top: 20px; margin-left: 5px; } .btn-gnavi span:nth-child(4) { display: inline-block; top: 20px; position: absolute; width: 0; height: 0; transition: all 400ms; white-space: nowrap; color: #ad9258; } .btn-gnavi.open { -webkit-transform: rotate(180deg); transform: rotate(180deg) } .btn-gnavi.open span:nth-child(4) { display: none; } .btn-gnavi.open span { background: #fff; } .btn-gnavi.open span:nth-child(1) { width: 24px; -webkit-transform: translate(-7px,17px) rotate(45deg); transform: translate(-7px,17px) rotate(45deg) } .btn-gnavi.open span:nth-child(3) { width: 24px; -webkit-transform: translate(-7px,-17px) rotate(-45deg); transform: translate(-7px,-17px) rotate(-45deg) }css
まとめ
ハンバーガーメニューを実装することで、スマホ画面のように
小さい画面でも見せたいメニューの表示場所を十分に確保できることがわかりました。
また、三本線だけだとわかりずらいので、三本線の下に『menu』と表示させ、メニューが収納されていることがユーザーに伝わるように工夫してみました。
- 投稿日:2020-03-14T13:43:11+09:00
「おにいちゃんのことなんか、全然好きじゃないんだからねっ...!」【ゼロからはじめるプロおにいちゃん】
はじめに
~我家の日常~
おにいちゃんの「よう妹。昨日夜遅くまで電話してて眠れなかったぞ。長電話もほどほどにしろよ」
妹「はぁ聞いてたの?マジできもいんですけど」
おにいちゃん「そんな言い方はないだろ。俺だってな...」
妹「うっさいからどっか行ってくれない。あたし忙しいんだけど」
おにいちゃん「.....。」
おにいちゃん (昔は「お兄ちゃんと結婚する」とか言ってくれてたのになぁ)我家の妹は思春期に入り、お兄ちゃんと会話はおろか同じ空間にいるだけで嫌な顔をされてしまう。
それもあってか昔みたいに仲良くしたいのに、嫌味ばかり言ってしまう悪循環。
そんな毎日に嫌気がさしていた。そんなある日のこと...いつものようにネットサーフィンをしていたらでこんなものを見つけた。
プロおにいちゃん育成API
おにいちゃん「本当に使えるのかこれ...。まぁ無料だし使ってみるか」プロおにいちゃんへの道
おれは妹にどんなことを言ってたっけな。
これまでのおにいちゃん
この前はこんな感じだったな
よう妹。昨日夜遅くまで電話してて眠れなかったぞどれどれ、どんなもんかな
$ node proBro.js よう妹。昨日夜遅くまで電話してて眠れなかったぞ 感情:Neutral 感情指数:0.2779170937476806別に悪いことは言ってなさそうだな...
考えるおにいちゃん
ただ思春期の妹ともなると、普通の言葉じゃ満足できないってわけか...
ならこんな感じで全力の愛を表現してやればいいのではないだろか。妹よ。お前の顔を見るだけで俺の胸は熱くなり、何も考えることができなくなる。 もう一度おにいちゃんを仲良くやっていこうじゃないかこれだけ熱い思いをぶつければ間違いなく...
$ node proBro.js 妹よ。お前の顔を見るだけで俺の胸は熱くなり、何も考えることができなくなる。もう一度おにいちゃんを仲良くやっていこうじゃないか 感情:Negative 感情指数:0.5なぜだ!!!
理解するおにいちゃん
ふむふむ。どうやら行き過ぎた言葉は重い感情として認識されるようだ。
ならば程よく、優しい言葉をかければちょうどよいのか。$ node proBro.js やぁ妹よ。今日の髪型はかわいいな。長澤まさみかと思ったよ 感情:Positive 感情指数:0.626925485337811(゚∀゚)キタコレ!!
完璧だ。これで妹と昔みたいに...プロおにいちゃん
~後日~
妹「どいて。邪魔なんだけど」
(今日はこいつを使って..)おにいちゃん「そんなこと言うなって。そういえば今日の髪型かわいいな。長澤まさみかと思ったよ」
妹「いきなりどうしたの?そんなこと言われたって..」
おにいちゃん (見える..見えるぞっ!!妹からの好感度が上がっていくのを!)感情:Positive 感情指数:0.626925485337811妹「...言いたいのはそれだけ?用がないならあたし行くけど」
おにいちゃん「待てって。今度お前が好きなアイスクリーム一緒に食べに行かないか?あそこのアイス昔から好きだもんな」感情:Positive 感情指数:0.4908134011319556妹「なんでそんな昔のこと覚えてるのよ...」
おにいちゃん「おれはプロおにいちゃんだからな。惚れたか?」
妹「バッカじゃないの?おにいちゃんのことなんか、全然好きじゃないんだからねっ...!」~Fin~
実装
全国の悩めるおにいちゃんのために、実装コードを配布したいと思います。
それでは皆様Good Sister Lifeを...
クリックでコード展開
proBro.jsconst axios = require('axios'); const msg = process.argv[2]; const BASE_URL = `[API Base URL]/nlp/v1/sentiment`; const options = { method: 'post', baseURL: BASE_URL, headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Bearer [your token]' }, data: { "sentence": msg, "responseType": "json" } }; const emotionCheck = async () =>{ try { const res = await axios.request(options); if(res.data.result.emotional_phrase.length <= 1){ for(let i = 1;res.data.result.emotional_phrase.length; i++){ if(/怒る|悲しい|不安|嫌|興奮|切ない|N*2/.test(res.data.result.emotional_phrase[i].emotion)) { console.log(`感情:Negative`); console.log(`気持ちの入り度:${res.data.result.score}`); }else{ console.log(`感情:${res.data.result.sentiment}`); console.log(`気持ちの入り度:${res.data.result.score}`); } } }else{ console.log(`感情:${res.data.result.sentiment}`); console.log(`感情指数:${res.data.result.score}`); } } catch (error) { console.log(error); } } emotionCheck();
まとめ
全おにいちゃんの悩める課題を解決するシステムが出来てしまいました。
このシステムを各家庭1つ配布することで、妹への想いの精度向上・悲しみを生まない世界へと変えることができるのではないでしょうか。
またこのシステムは皆様自由にカスタマイズすることも可能ですので、よりよい妹ライフプロおにいちゃんライフを送ることができるよう協力をお願いします。またこの記事を見たおにいちゃんの達が幸せになることを心より願っております。
- 投稿日:2020-03-14T11:20:13+09:00
【 2020/03/31まで ! 】インプレスで無料公開中の書籍をPDF化して読む!
概要
コロナウイルス対策で増えた在宅時間を生かすために、インプレスさんが「できる」シリーズなど44冊を2020/03/31まで無料公開してるよ!
どの書籍も面白そうで全部読みたいけど、本気を出してもあと2週間では読みきれないし、保存してゆっくり読もうと思う
警告
- ダウンロードした画像、作成したPDFは私的利用に限定してください。他人への販売や譲渡は犯罪です。
- この記事は、著作権法とimpressの利用規約を確認した上で、問題ないと判断して書いています。
- 自己責任でお願いします。
分かりやすく書きすぎてリテラシーがない人に悪用されると困るので、環境構築やパッケージのインストールは省略した上で、ステップごとに分散して書いてます。
STEP 1. 無料公開されてる本とそのリンクを取得してみる!
まずは、無料公開されてる本と、無料公開のリンクをオブジェクトにして取得するよ。無料公開の特設ページ(https://book.impress.co.jp/items/tameshiyomi)にアクセスしてから、デベロッパーツールのConsoleを開いて、以下のコードを実行しよう。
const title = [...document.querySelectorAll('h4 a')].map(i => i.innerHTML); const num = [...document.querySelectorAll('.module-book-list-item-img div a')].map(i => i.href.split('/')[3]); if (title.length !== num.length) { throw new Error('タイトルの数とリンクの数が違います。'); } const title_and_num = {}; for (const i in title) { title_and_num[num[i]] = title[i]; } document.body.innerHTML = '<pre>' + JSON.stringify(title_and_num, null, '\t') + '</pre>';すると、画面が書き換えられて、オブジェクトが表示されるよ。
キーになっている数字はそれぞれのページの末尾を表してるから、
url = 'https://impress.tameshiyo.me/' + '9784295003850'みたいにしてURLを求めるよ。
コピペして
impress_title_num.json
っていうファイルに保存しておいてね。STEP 2. 画像のURLを取得する!
PythonでSeleniumを使用して全ての画像のURLを取得して、jsonファイルを作成するよ。URLを取得するコードは、サーバへの負荷も考慮し、1リクエストごとに5秒以上待つようにしてあります。
from selenium import webdriver from selenium.webdriver.chrome.options import Options import chromedriver_binary from time import sleep import json url_num = input('url_num: ') url = 'https://impress.tameshiyo.me/' + url_num urls_image = [] options = Options() options.binary_location = '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary' options.add_argument('--headless') driver = webdriver.Chrome(options=options) driver.get(url + '?page=1') sleep(5) # 表示されている総見開き数 count_of_view = int(driver.find_element_by_id('page_indicator').text.split(' ')[2]) # 総ページ数は、count_of_view * 2 - 1 または、count_of_view * 2 - 2 になる。 count_of_page = count_of_view * 2 for i in range(count_of_page): page = i + 1 # 1, 3, 5の偶数ページ(左側のページ)のとき、新しいページを取得。 if page % 2 == 0: # URLのページ数は実際のページ数より1つ多い(表紙の前の存在しないページを1ページ目としている) driver.get(url + '?page=' + str(page+1)) sleep(3) # ページ数が偶数の時は最初の要素、奇数の時は2つめの要素にURLが含まれる。 number_of_image_url_place = page % 2 try: img_url = '' try_counter = 0 while img_url == '': try_counter += 1 sleep(0.5) img_url = driver.find_elements_by_class_name('page_img')[number_of_image_url_place].get_attribute('src') # 10回試行してsrc無し、かつ、ページ数が最後の2ページなら、ページがないと判断 if try_counter > 10 and page >= count_of_view * 2 - 2: break # 代理画像URLが取得されている場合は警告を出す if img_url == 'https://impress.tameshiyo.me/img/bookfilter.png': print('[warning] img_url is bookfilter in page ' + page) elif img_url == '': break else: urls_image.append(img_url) print('finished page ' + str(page)) except: print('can\'t get url on page ' + str(page)) pass driver.close() driver.quit() title_and_num = json.load(open('impress_title_num.json', 'r', encoding='utf-8')) with open('impress_urllist_' + title_and_num[url_num] + '.json', 'w', encoding='utf-8') as f: f.write(json.dumps(urls_image, indent=4))確認
画像にはCORSの設定がされていないようなので、URLが正しく取得されているか、下のようなHTMLファイルを使って確認してみよう!
注意 : このHTMLファイルを不特定多数の人がアクセスできるサーバーに配置しないでください。著作権侵害となる可能性があります。
<!DOCTYPE html> <html> <body> <input type="text" placeholder="数字を入力してね"> <button type="button">決定</button> <script> fetch('impress_title_num.json') .then(a => a.json()) .then(titleAndNum => { document.querySelector('button').addEventListener('click', () => { const title = titleAndNum[document.querySelector('input').value]; console.log(title) const fileName = `impress_urllist_${title}.json`; fetch(fileName) .then(b => b.json()) .then(json => { for (const i in json) { const img = document.createElement('img'); img.src = json[i]; document.body.appendChild(img); } }); }) }); </script> </body> </html>Macの場合は、PHPでローカルサーバを立てて確認するのが楽だよ。
$ php -S localhost:8080STEP 3. 画像を保存する!
画像を保存っていうと著作権大丈夫?って感じがするけど、普段ブラウザでみている画像も一時的にローカルに保存されているので、保存している場所が違うだけだよ。
ここでも、1枚保存するごとに3秒間隔をおいてます。import urllib.request, urllib.error import json, os from time import sleep title_and_num = json.load(open('impress_title_num.json', 'r', encoding='utf-8')) num = input('input num : ') title = title_and_num[num] url_list = json.load(open('impress_urllist_' + title + '.json', 'r', encoding='utf-8')) os.makedirs('impress_' + title) for i, url in enumerate(url_list): urllib.request.urlretrieve(url, './impress_{0}/impress_{1}_{2}.jpg'.format(title, num, ('00' + str(i+1))[-3:])) print('[done] impress_{0}_{1}.jpg'.format(num, ('00' + str(i+1))[-3:]))STEP 4. PDF化する!
ダウンロードした画像をくっつけてPDFにするよ。
import img2pdf from pathlib import Path import json num = input('input num : ') title_and_num = json.load(open('impress_title_num.json', 'r', encoding='utf-8')) title = title_and_num[num] path_import = Path('impress_' + title) path_output = Path('impress_' + title + '/' + title + '.pdf') lists = list(path_import.glob('**/*')) with open(path_output, 'wb') as f: f.write(img2pdf.convert([str(i) for i in sorted(lists) if i.match('*.jpg')]))いつもの見慣れたPDFビューアだ!これで時間を気にせず引きこもれるぞ!
インプレスさんありがとう!
- 投稿日:2020-03-14T03:42:09+09:00
MDN web docsにあるデモと同じように、console.logの中身を掠め取って綺麗に表示するものを作りたい。
趣味でプログラミングやってる暇人の遊びですので、出来は全く保証できません。あしからず。
MDN web docsにあるデモってなに?
これ
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from作る前の考察
上記の写真から分かる大まかな構造:
型判定し、型毎に分岐する関数a Boolean、Null、Undefined、Number、String等: ┗String()で文字化 Array: ┗Arrayを解析する関数bを呼び出し (中身に応じてaを再帰的に呼び出し) Objectやその他: ┗Objectを解析する関数cを呼び出し (中身に応じてaを再帰的に呼び出し) console.logの引数を横流しする仕組みこの順番に書いていきます。
型判定する関数a
function a(x) { var t = typeof x; if(t == "boolean" || x == null || x == undefined || t == "number" || t == "symbol") { return String(x); } else if(t == "bigint") { return String(x) + "n"; } else if(t == "string") { return "\"" + String(x) + "\""; } else if(Object.prototype.toString.call(x) === "[object Array]") { return "Array " + b(x);//配列を解析する関数 } else { return c(x);//オブジェクトを解析する関数 } }配列を解析する関数b
function b(x) { var ary = []; for(var y of x) { ary.push(a(y));//再帰的にaを呼び出し } var r = "[" + ary.join(", ") + "]"; return r; }オブジェクトなどの残りを解析する関数c
オブジェクトは種類がいっぱいあり、辞書型なオブジェクトとそうでないものを分ける必要がある。安易に文字形式にできないワケワカメなオブジェクトもあるので、それらは型名だけ吐かせる。Object.prototype.constructor.nameでゴリゴリ種類を判別する。
function c(x) { var n = x.constructor.name; var r = ""; /*オブジェクト*/ if(n == "Object") { var ary = []; for(var k in x) { ary.push(k + ": " + a(x[k]));//再帰的にaを呼び出し } r = "Object {" + ary.join(", ") + "}"; } /*構造化データ*/ else if(n == "ArrayBuffer" || n == "SharedArrayBuffer" || n == "DataView") { r += n + "(" + x.byteLength + ") {}"; } /*索引付きコレクション*/ else if(n.match(/^.*?Array$/)) { r += n + "(" + x.length + ") " + b(x);//配列と同じようなものなので、配列解析に任せてしまいます } /*Promise(どうあがいても中身を取り出せなかったので、中身は無かったことに…。)*/ else if(n == "Promise") { r += n + " {}"; } /*(本当は他にも例外処理をしないといけないものがある気がするものの、*/ /* オブジェクトの種類が膨大なので、この辺りで諦めました。)*/ /*残りを文字化*/ else { r += String(x); } return r; }console.logの引数を横流しして完成
/*改変前に退避*/ var _l = console.log; console.log = function() { var t = []; for(var arg of arguments) { var o = a(arg);//型判定関数に引数を受け渡し t.push(o); } var c = t.join(" ");//結果 document.getElementById("result").innerHTML += "\n> " + c + "\n"; _l.apply(console, arguments); }<pre id="result"></pre>動かすと…?
JavaScriptconsole.log(0); console.log("0"); console.log([0, 1, function() {}]); console.log({a: void 0}); console.log({a: {a: [0, 1, 2]}}); console.log(new RegExp("^a", "ig")); console.log(new Int16Array(2)); console.log(new ArrayBuffer(2));とすると、
> 0
> "0"
> Array [0, 1, function() {}]
> Object {a: undefined}
> Object {a: Object {a: Array [0, 1, 2]}}
> /^a/gi
> Int16Array(2) [0, 0]
> ArrayBuffer(2) {}
となる。
参考文献
標準ビルトインオブジェクト
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects
by MDN contributors
CC BY-SA 2.5 ライセンス
- 投稿日:2020-03-14T02:03:36+09:00
ブロックチェーンのブロックを検証し、ハッシュを求める
みなさん、改ざんに強く信頼性が高いと言われるブロックチェーンですが、それではブロックチェーンのブロックを検証したことがありますか?
僕はあります
ぜひ、みなさんにも体験して欲しいです。
なので今日はNEM Catapultエンジンを搭載したSymbolブロックチェーンのテストネットを使ってブロックを検証してみましょう。必要なもの
ネットワーク環境
Chromeブラウザ前準備
デベロッパーツールを起動します。F12 あるいは fn+F12 などでコンソール画面を開いてください。以下のような画面が表示されます。
>という入力受付モードになっているかと思います。ここにjavascriptを入力すると、対話式に動作結果を確認することができます。この表示になっていない場合は Console タブを選択し直してみてださい。
次にsymbol-sdkを読み込みます。awsにsymbol-sdk-typescriptをbrowserify化したものを置いているのでそれを読み込みにいきます。(現在の最新バージョンは0.17.3です)
https://s3-ap-northeast-1.amazonaws.com/xembook.net/nem2-sdk/symbol-sdk-0.17.3.js(script = document.createElement('script')).src = 'https://s3-ap-northeast-1.amazonaws.com/xembook.net/nem2-sdk/symbol-sdk-0.17.3.js' document.getElementsByTagName('head')[0].appendChild(script);symbol-sdkをインポートします。
nem = require("/node_modules/symbol-sdk");今回はopening-lineさんのノードをお借りします。築城された方はご自身のノードIPを指定してみてもいいかもしれません。
NODE = "https://sym-test.opening-line.jp:3001";これでブロックチェーンにアクセスする準備ができました。
では検証してみたいブロックを表示してみましょう。今回はブロック高=2のブロックを表示してみます。new nem.BlockHttp(NODE).getBlockByHeight(2).subscribe(x=>console.log(x))以下のように出力されました。これが、キャッシュされていない「生のブロックチェーン」です。
署名検証
それでは署名検証に必要な情報を集めていきましょう。何が必要かはSymbolテクニカルリファレンスの 7.1ブロックフィールドに記載されています。
NEM Symbol Technical Reference
【NEM技術勉強会】7.1 ブロックフィールド【カタパルト白書】(b)のVerifiable dataが検証に必要なデータです。これらをSignerの秘密鍵で署名するとSignatureが生成されます。そして、Signerの公開鍵を使うとSignatureの署名者が検証できます。
V,N,Tは
version,networkType,EntityTypeでそれぞれ1,152,33091になります。
そのほかはそのまま該当するキー値があります。
検証にはそれぞれが固定幅を持つデータ列にシリアライズする必要があります。
そこで固定幅のサイズをcatbufferで調べてみます。
catbuffer とはおそらく Catapult ByteBufferの略と思われます。catbuffer/schemas/entity.cats
enum EntityType : uint16
enum NetworkType : uint8
version = uint8catbuffer/schemas/types.cats
using BlockFeeMultiplier = uint32
using Difficulty = uint64
using Height = uint64
using Timestamp = uint64その他の項目についてはhash値なのでそのままシリアライズします。
次にシリアライズするためにcatbufferというライブラリを読み込みます。
(script = document.createElement('script')).src = 'https://s3-ap-northeast-1.amazonaws.com/xembook.net/nem2-sdk/catbuffer-typescript-0.0.11.js' document.getElementsByTagName('head')[0].appendChild(script);インポート
cat = require("/node_modules/catbuffer-typescript");cat.GeneratorUtils.uintToBuffer
cat.GeneratorUtils.uint64ToBufferという関数が使えるようになります。
シリアライズ時に配列の箱に情報を詰め込んで行くのですが、8箱使うものについてはuint64ToBuffer([lower,heigher])をそれ以外のものはuintToBuffer(,箱数)を使いましょう。hash値のシリアライズ変換についてはsymbol-sdkのConvert.hexToUint8を使用します。ブロック高=2のデータについては以下のように詰め込むことができました。
v = cat.GeneratorUtils.uintToBuffer(1, 1) //version:8 n = cat.GeneratorUtils.uintToBuffer(152, 1) //network:8 t = cat.GeneratorUtils.uintToBuffer(33091, 2) //type:16 h = cat.GeneratorUtils.uint64ToBuffer([2,0]) //height ts = cat.GeneratorUtils.uint64ToBuffer([1463715346,2]) //timestamp df = cat.GeneratorUtils.uint64ToBuffer([276447232, 23283]) //difficulty ph = nem.Convert.hexToUint8("2D57E4CAB3F3DEF62C4DC8F7DDEB22E85AE89E350B2E91CE208FCA5AEE32076C") //prehash th = nem.Convert.hexToUint8("0000000000000000000000000000000000000000000000000000000000000000") //txhash rh = nem.Convert.hexToUint8("08E9093BFE207E0045EBE06612F0882EEE761A86FC075EB442DCB26F80BEEA48") //rcpthash sh = nem.Convert.hexToUint8("7EE8643A91DECA208C749A23BBEBF7046DA653B874407AAB32B3AFD5F0080A35") //stathash bp = nem.Convert.hexToUint8("B4D61012F42CB0AA654B9631C25FAD5AD5CF52A57FA25BA0ACC0019B220B2CE8") //benepubkey fm = cat.GeneratorUtils.uintToBuffer(0, 4) // feeMultiplier:32次にそれぞれを配列結合します。
buffer = Array.from(v) .concat(Array.from(n)) .concat(Array.from(t)) .concat(Array.from(h)) .concat(Array.from(ts)) .concat(Array.from(df)) .concat(Array.from(ph)) .concat(Array.from(th)) .concat(Array.from(rh)) .concat(Array.from(sh)) .concat(Array.from(bp)) .concat(Array.from(fm));192個の配列に結合されました。次に16進数の文字列に変換します。
> hex = nem.Convert.uint8ToHex(buffer); < 01984381020000000000000012863E570200000000407A10F35A00002D57E4CAB3F3DEF62C4DC8F7DDEB22E85AE89E350B2E91CE208FCA5AEE32076C000000000000000000000000000000000000000000000000000000000000000008E9093BFE207E0045EBE06612F0882EEE761A86FC075EB442DCB26F80BEEA487EE8643A91DECA208C749A23BBEBF7046DA653B874407AAB32B3AFD5F0080A35B4D61012F42CB0AA654B9631C25FAD5AD5CF52A57FA25BA0ACC0019B220B2CE800000000署名者が署名したかどうか公開鍵で検証します。
> signer = nem.PublicAccount.createFromPublicKey("B4D61012F42CB0AA654B9631C25FAD5AD5CF52A57FA25BA0ACC0019B220B2CE8"); > signer.verifySignature(hex,"BD019475506D1FD1F330F580F8A32EC35329540758148CFB181C881FB8416D6C05D208008325ADB3FAA89B21E768FBEABB514F7458A5EEFC3BEDA7187E244200"); < true公開鍵で署名者のアカウントを生成しsymbol-sdkのverifySignatureで署名されるデータと署名データを公開鍵で比較します。
trueと出力されました。検証完了です。間違いなくこのアカウントによって署名されたことが証明できました。ハッシュ生成
ハッシュライブラリをインポートして、sha3のハッシュクラスを生成します。
jssha3 = require('/node_modules/js-sha3'); hasher = jssha3.sha3_256.create();hashの生成順序はここに記載されています。
catapult-server/src/catapult/model/EntityHasher.cpp
sha3.update(entity.Signature); sha3.update(entity.SignerPublicKey); sha3.update(buffer); sha3.final(entityHash);最後のbufferについてはどの範囲のデータか悩みましたが、結果として検証データと同じもので大丈夫でした。
最初に署名でハッシュ更新します
hasher.update(nem.Convert.hexToUint8( "BD019475506D1FD1F330F580F8A32EC35329540758148CFB181C881FB8416D6C05D208008325ADB3FAA89B21E768FBEABB514F7458A5EEFC3BEDA7187E244200")); //signature次に公開鍵で更新
hasher.update(nem.Convert.hexToUint8( "B4D61012F42CB0AA654B9631C25FAD5AD5CF52A57FA25BA0ACC0019B220B2CE8")); //publicKey最後に署名データで更新
hash = hasher.update(buffer).hex().toUpperCase(); < "441F4C83B551E78532810DE2BC42B976B9370D25BA62F1B184A658475848DE2C"出力された値と最初に表示したblock高=2のハッシュ値と比較して同じであれば、ハッシュ値はデータを正しく要約していると考えられます。
これで、ブロックチェーンのブロックの署名検証とハッシュ生成を追体験することができました。
ブロックチェーンが改ざんに強いと言われるのはこれらの仕組みがうまく機能し、ノード間がお互いを検証し合っているからです。Catapultエンジンを搭載したブロックチェーンではなクライアントからでも簡単に検証することができます。将来、IoTがブロックチェーンと深く結びついてくるとブロックヘッダーを保存したクライアントが活躍するようになると思いますが、これはまた別の機会にお話します。さあ、あなたも今日からこう言えます。
「キミ、ブロックチェーンを熱く語ってとても信頼しているようだけど、検証したことある?」
僕はあるよ
- 投稿日:2020-03-14T01:57:28+09:00
jQuery+Colorboxでaタグ無しで画像のモーダル表示を実現する簡単な方法
画像のモーダル表示のためにLightbox系のライブラリの使い方を検索してみると、モーダル表示したい画像のimgタグをaタグで囲むようなものばかりが検索結果として出てきます。
しかし、CMS等の環境の制約によりaタグでimgタグを囲めない場合や、メンテナンス効率の低下を避けるためにaタグでimgタグを囲みたくない(aタグとimgタグの両方をメンテナンスしたくないし、それ以前に入力が面倒なのでやりたくない)場合があると思います。
そのような場合は、JavaScriptで画像(img)のクリックイベントを取得し、クリックされた画像のsrcを取得し、その値をColorboxのようなモーダル表示時に表示させる画像のリンク先(パス、URL)を指定可能なライブラリへ受け渡すことで、imgタグをaタグで囲むことなく、画像のモーダル表示を行うことができます。
※事前にjQueryとColorboxのファイル一式を用意(アップロード)しておきます。
<script src="/lib/jquery/jquery.min.js"></script> <script src="/lib/jquery/jquery-migrate.min.js"></script> <script src="/lib/colorbox/jquery.colorbox-min.js"></script> <link rel="stylesheet" href="/lib/colorbox/example4/colorbox.css" media="all"> <script type="text/javascript"> $(document).ready(function() { $("#content img").css("cursor","pointer"); $("#content img").click(function() { $.colorbox({href:this.src}); return false; }); }); </script>関連URL
- 投稿日:2020-03-14T01:46:27+09:00
ブックマークレットで13日の金曜日を数える
この記事は
2020年3月13日が13日の金曜日だったので、javascriptで13日の金曜日を数えてみました。
自分の誕生日から今日までに何度13日の金曜日を過ごしたか確認できます。コード
friday_13th.jsjavascript:(function(){ let inputDate=prompt('誕生日は?', '2012/12/12'); //yyyy/m/d let birthday = new Date(inputDate); let today = new Date(); let jasonDate = []; const listOf13th = (year)=>{ return Array(12).fill().map((_, i) => new Date(`${year}/${i+1}/13`)); } for(let y=birthday.getFullYear(); y<=today.getFullYear(); y++){ let fridays = listOf13th(y).filter( (d)=> (birthday < d && d < today && d.getDay() == 5) ); jasonDate.push(...fridays); } alert('あなたは' + jasonDate.length + '回、13日の金曜日を生き抜きました。'); console.table(jasonDate.map( (v,i)=>{ return {date:v.toLocaleDateString()}; })); }) ()まとめ
何事もなく過ごせました。よく頑張った。
- 投稿日:2020-03-14T00:46:32+09:00
初心者のためのダイクストラアルゴリズム
ダイクストラのアルゴリズムってあるじゃないですか?名前も覚えにくいしそもそも重み付きグラフの最短経路問題とか実務で実装することそうそうないのですぐ忘れちゃうんですよね。。。
ダイクストラアルゴリズムとは
ダイクストラアルゴリズムとはグラフの最短経路を求めるアルゴリズムです。名前だけは聞いたことのある方も多いかと思います。やってることはイージーでシンプルなんですが少しとっつきにくいですが一つ一つ理解していけば割とわかりやすいアルゴリズムです。
重み付き出ない、迷路の探索などは幅優先探索で解けるのですが、各辺に重みがついてる場合は結局全通りを計算しないといけないことになります。全ての頂点を一回だけ通るとして、辺E個あるとすればO(E!)となり計算量は爆発してしまします。
これだと計算するにも少し厳しいです。そんな問題を効率的にといてくれるアルゴリズムそれがダイクストラアルゴリズムです。
ちなみに各辺のコストは非負の値(0以上)でなければなりません。負の数が含まれてる場合はベルマン-フォード法などを使用することになります。
手順
ダイクストラ法の手順はかなり単純です。
- 始点に最短距離0を設定する
- まだ辿ってない点の中から最短距離側家庭て最も距離が短い頂点に移動する
- その頂点から繋がっている頂点の最短距離を設定する。この時にその頂点の最短距離を更新できるなら更新する。
- これを全ての頂点の最短距離をわかるまで行う
さてと言われても少し難しいと思いますので、実例をみていきましょう。
例
なんともアナログな手法ですが一番表現できたので。。。
以下のような図の最短経路を考えていきます。
緑が最短経路が確定して移動ずみの頂点となります。赤が起点となる頂点です。各頂点の数字はその時の最短経路となります
さてみていただくと分かるようにスタート地点から順番にその時点での最短の頂点に移動してそこからまた隣り合う頂点の最短経路を計算しているのが分かるかと思います。
実装
さて実装に入っていきましょう。
上の手順を愚直に実装してみます。function main(nodes) { const start = nodes[0] // 訪問済みの頂点を記録 const visited = new Set() const routesFromStart = new Map() // 始点からの距離の記録 routesFromStart.set(start, {distance: 0}) for(const n of nodes) { if(n != start) { // スタート以外の全ての頂点に無限大を代入 routesFromStart.set(n, {distance: Number.MAX_VALUE}) } } let current = start let routes = new Map() while(current != null) { visited.add(current) for(const edge of current.edges) { // その頂点から隣り合う頂点の最短距離を計算して、計算済みの値より低ければ更新 if(edge.cost + routesFromStart.get(current).distance < routesFromStart.get(edge.to).distance) { routesFromStart.set(edge.to, {distance: edge.cost + routesFromStart.get(current).distance}) routes.set(current, edge.to) } } let cheapestNodeDistance = Number.MAX_VALUE current = null // 訪問してない最短距離を計算済みの頂点の中から最小の頂点を選ぶ for(const city of routesFromStart.keys()) { if(!visited.has(city) && cheapestNodeDistance > routesFromStart.get(city).distance){ cheapestNodeDistance = routesFromStart.get(city).distance current = city } } } return routesFromStart.get(nodes[nodes.length - 1]).distance }このコードは各頂点をVとすると最大で各辺の個数を回るループの中で中で最小の頂点を選ぶためのループを回してるので計算量はO(V^2 + E)メモリについては各頂点分の記録を行わないといけないのでO(V)となります。
Priority Queueを用いた実装について
さて感のいい人ならお気づきになったかもしれませんが、このコード実は最小の頂点を決めるロジックを最適化することができます。それが優先度付きキューです。
優先度付きキューは挿入、取り出しにO(logN)の計算量が必要となりますが、最小の頂点を決める際の計算量が線形の探索より早くなります。Priority QueueはJavaScriptには標準で実装はされてないので、Pythonでの実装です。
def dijkstra(nodes): start_node = nodes[0] routes_from_start = {n: math.inf for n in nodes} # 最初の頂点にゼロを設定 routes_from_start[start_node] = 0 minHeap = [] # 最初の頂点を追加 heappush(minHeap, (0, start_node)) # ヒープがなくなるまで探索 while minHeap: (cost, current_node) = heappop(minHeap) # priority keyは重複するのでここでチェックする if cost > routes_from_start[current_node]: continue for node in current_node.routes: price_info = current_node.routes[node] if routes_from_start[node] > price_info + routes_from_start[current_node]: routes_from_start[node] = price_info + routes_from_start[current_node] # 更新されたらpriorityに値を追加 heappush(minHeap, (price_info + routes_from_start[current_node], node)) return routes_from_start[nodes[-1]]Priority Queueの説明はまたの機会にしましょう。
より計算効率がよく各頂点V, 各辺をVとするとVをmapに設定するO(V)とEの回数分heapを操作するのでO(ElogE)。の合計O(V + ElogE)で求まることがわかります。これは最初のアルゴリズムより効率的です。経路を記憶する
さて最短のコストはわかりました。しかしこの問題は"最短経路"問題です。最短のコストが求まったらその経路も知りたくなるのが普通です。
上のコードを改善してみましょう。def dijkstra(nodes): start_node = nodes[0] routes_from_start = {n: math.inf for n in nodes} # 最初の頂点にゼロを設定 routes_from_start[start_node] = 0 minHeap = [] # 最初の頂点を追加 heappush(minHeap, (0, start_node)) path = collections.defaultdict(Node) # ヒープがなくなるまで探索 while minHeap: (cost, current_node) = heappop(minHeap) # priority keyは重複するのでここでチェックする if cost > routes_from_start[current_node]: continue for node in current_node.routes: price_info = current_node.routes[node] if routes_from_start[node] > price_info + routes_from_start[current_node]: routes_from_start[node] = price_info + routes_from_start[current_node] # 最短距離を更新するノードを記録する path[node.id] = current_node.id # 更新されたらpriorityに値を追加 heappush(minHeap, (price_info + routes_from_start[current_node], node)) current_node = nodes[-1].id path_array = [] #最短距離を記録したノードをゴールからたどる while current_node: path_array.append(current_node) if current_node not in path: break current_node = path[current_node] return routes_from_start[nodes[-1]], path_array[::-1]ダイクストラアルゴリズムでは最短距離を更新するノードが分かるのでそれを記録して最後に辿ればよいことになります。
計算量は最短距離のノードの数分増えてしまうことになります。ところでなんでこれで最短経路が求まるのか
さてここまでみてきて多くの人はこう思ったのではないでしょうか?確かにアルゴリズムは簡単だしそれを実装するのもそんなに難しくはない。でもなんで最短距離が求まるの?軽く確認していきましょう
Lに入っている頂点はスタートSからの最短距離であると仮定して、そこから繋がる最短の頂点がまたSから最短距離であることを言えたら良さそうですね。
さてTに含まれるうちの最短の頂点に移動するので、最小の点をiとするとd[i] = min(T)ですよね。
さてここで各頂点をkとすると最短距離d[k] は d[k] >= d[i]であることは確定しますよね。d[i] は最小の点であり、各頂点は非負なので。
と次々やっていくと帰納法的に証明できます。さてこれってよく考えたら漸化式ですよね。
d[i] = min(k ⊂ T) + iに隣接するLの頂点の最短距離
漸化式の時には動的計画法が
漸化式ときたらDPですよね。DPについてはこの記事がすごく参考になります(https://qiita.com/drken/items/a5e6fe22863b7992efdb)
このような感じで値が更新されていきます。縦軸は試行の回数。横軸は頂点です。
なんだダイクストラアルゴリズムはDPの一種だったんだ。
まとめ
さてみてきましたダイクストラのアルゴリズムですが、一回理解してしまうと結構簡単に理解できます。あとは実装してみてアルゴリズムの問題で類似の問題に当たった時にあこれはあの時の!!という感じで解いていきたいものです。
*余談ですが僕もDPの記事書きたい参考
http://www.lab2.kuis.kyoto-u.ac.jp/~shuichi/algintro/alg-6s.pdf
https://www.youtube.com/watch?v=X1AsMlJdiok
- 投稿日:2020-03-14T00:20:05+09:00
React Navigation v5 が推奨する認証フローの実装
はじめに
前回の記事に続いて、 React Navigation v5 に関する記事です。
Version 5.x では、公式ドキュメントで推奨の認証フローが紹介されています。
今回は、その認証フローの実装方法に関して記事にまとめました。下準備
認証フローを実装するにあたり、アプリ全体で状態を管理する必要があります。
この記事では、 Context API を利用して実装します。型定義
利用する型を定義します。
必要に応じて、Error
などの状態・処理も追加してください。type State = | { status: 'Unauthenticated'; token: string } | { status: 'Loading';} | { status: 'Authenticated'; token: string }; type Action = | { type: 'START_LOGIN' } | { type: 'COMPLETE_LOGIN'; token: string } | { type: 'COMPLETE_LOGOUT' }; type Dispatch = (action: Action) => voidReducer
各アクションにおける取りうる状態を定義します。
const authReducer = (prevState: State, action: Action): State => { switch (action.type) { case 'START_LOGIN': return { ...prevState, status: 'Loading', }; case 'COMPLETE_LOGIN': return { ...prevState, status: 'Authenticated', token: action.token, }; case 'COMPLETE_LOGOUT': return { ...prevState, status: 'Unauthenticated', token: undefined, }; } };コンテクスト
アプリの状態、ディスパッチャーそれぞれのコンテクストを定義します。
const AuthStateContext = createContext<State>({ status: 'Unauthenticated', token: undefined, }); const AuthDispatchContext = createContext<Dispatch | undefined>(undefined);自作 Hooks
慣習に従い、
useXXXX
の形で Hooks を定義します。export const useAuthState = () => { const context = React.useContext(AuthStateContext); return context; }; export const useAuthDispatch = () => { const context = React.useContext(AuthDispatchContext); return context; };プロバイダー
アプリ全体で状態を扱えるように、プロバイダーを定義し、値を伝播させます。
interface Props { readonly children: React.ReactNode; } const AuthProvider: React.FunctionComponent<Props> = ({ children }) => { const [state, dispatch] = useReducer(authReducer, { status: 'Unauthenticated', token: undefined, }); return ( <AuthStateContext.Provider value={state}> <AuthDispatchContext.Provider value={dispatch}> {children} </AuthDispatchContext.Provider> </AuthStateContext.Provider> ); };定義したプロバイダーをアプリに適用します。
const App: React.FunctionComponent = () => { return ( <AuthProvider> <NavigationContainer> ... </NavigationContainer> </AuthProvider> ); };ここまでで状態管理をするためのプロバイダーが準備できました。
子コンポーネントでuseAuthState
とuseAuthDispatch
が扱えるようになりました。これで準備は完了です。
認証状態による表示画面の出し分け
認証処理を実装します。
const Stack = createStackNavigator(); const StackNavigator = () => { const state = useAuthState(); if (state.status === 'Loading') { return <LoadingScreen />; } return ( <Stack.Navigator headerMode="none" screenOptions={{ animationEnabled: false }} > {state.status === 'Authenticated' ? ( <Stack.Screen name="Home" component={HomeScreen} /> ) : ( <Stack.Screen name="SignIn" component={SignInScreen} /> )} </Stack.Navigator> ); };
useAuthSate
でアプリの状態を取得することができます。
上記の例では、状態を元に処理を分けています。状態が
Loading
の場合(データの取得中など)には、LoadingScreen
を表示します。
それ以外の状態の時は、Stack.Navigator
を表示します。ナビゲーターを表示する際には、状態が
Autheticated
(認証済)の場合はHomeScreen
を、それ以外の場合はSignInScreen
を表示するように処理しています。初期状態は
Unauthenticated
からスタートします。
そのため、SignInScreen
に遷移します。では、それぞれの画面はどのようになっているのか、見ていきます。
各画面の実装
ログイン画面
const SignInScreen: React.FunctionComponent = () => { const dispatch = useAuthDispatch(); return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Button title="SignIn" onPress={() => dispatch({ type: 'COMPLETE_LOGIN', token: 'dummy-token' }) } /> </View> ); };ここではログインボタンだけを実装しています。
ログインボタンを押した際、アプリにCOMPLETE_LOGIN
の状態をディスパッチします。
※本来は認証チェックをすべてパスした場合に、状態をディスパッチすべきですが今回は省略します。これにより、状態は
Authenticated
へと変化します。新しい状態に変化すると、
state
が更新されて、再度レンダリングが行われます。
status
はAuthenticated
になっているため、今度はHomeScreen
にナビゲーションされます。ホーム画面
const HomeScreen: React.FunctionComponent = () => { const dispatch = useAuthDispatch(); return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Button title="SignOut" onPress={() => dispatch({ type: 'COMPLETE_LOGOUT' })} /> </View> ); };ここではログアウトボタンだけを実装しています。
ボタンを押下すると、ログアウトした状態(COMPLETE_LOGOUT
)がディスパッチされます。これにより、再びログインページへと遷移します。
おわりに
いかがだったでしょうか。
今回は複雑な処理は省略しましたが、状態でStack.Screen
を制御することで許可されていないスクリーンに遷移することを防ぐことができます。また、サーバーとの通信中はローディング画面を表示する、など柔軟に表示内容を変更することができます。
React Navigation で認証ありのアプリを作成する場合、この認証処理も選択肢としてはありかもしれません。
もし記事に関して何かありましたら、ぜひコメントをよろしくお願いします。