- 投稿日:2020-01-19T23:22:05+09:00
JavaScript勉強の記録その22:setTimeoutとclearTimeoutを使って関数を呼び出す
setTimeoutを使って関数を呼び出す
setInterval(関数名,ミリ秒)とすることで、特定のメソッドを指定したミリ秒間隔で呼び続けることができますが、setTimeoutはsetTimeout(関数名,ミリ秒)とすることで、特定のメソッドをミリ秒後に1度だけ呼ぶことができます。
例えば以下の例では、ボタンがクリックされてから、2秒後にDateオブジェクトが生成されてコンソールに表示されています。
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Javascript Practice</title> </head> <body> <button type="click" id="button">showDatePerSecondを呼ぶ</button> <script src="main.js"></script> </body> </html>index.jsfunction showDateTimePerSecond() { console.log(new Date()); }; document.getElementById('button').onclick = function(){ setTimeout(showDateTimePerSecond,2000); }setTimeout(関数名,ミリ秒)で繰り返し関数を呼ぶ
setTimeoutもsetIntervalと似たような使い方ができます。
以下の例では、showDateTimePerSecondの関数内でsetTimeoutを呼び、引数にshowDateTimePerSecondを取ることで繰り返しの処理をしています。index.jsfunction showDateTimePerSecond() { console.log(new Date()); setTimeout(showDateTimePerSecond, 1000); }; showDateTimePerSecond();clearTimeout()で処理を止める
setTimeoutの繰り返しを止めるには、clearTimeoutというメソッドが用意されています。
以下の例では変数iが3になった時点でclearTimeoutを呼んで処理を止めています。index.jslet i = 0; function showDateTimePerSecond() { console.log(new Date()); const timeoutId = setTimeout(showDateTimePerSecond, 1000); i ++; if (i > 2) { clearTimeout(timeoutId); } }; showDateTimePerSecond();setIntervalとsetTimeoutの違い
setIntervalで繰り返し処理を行うのと、setTimeoutで繰り返し処理を行うのではどのような違いがあるでしょう?
以下のサイトがとてもわかりやすいです。要は、
setInterval ← 呼び出しを行なった関数の処理終了は関係なく、指定された時間になったら問答無用で引数で渡されている関数を呼ぶ。
setTimeoutで繰り返し処理 ← 呼び出しを行なった関数の処理が終わった後、指令された時間を置いて、引数で渡されている関数を呼ぶ。
- 投稿日:2020-01-19T23:17:04+09:00
Node.js install for mac
何からNode.jsを入手したか忘れないためのメモ。
- 公式からLTS版mac用のinstallerをダンロードする
- installerを起動し、各設問をデフォルトのまま進めてinstallする
- nodeのバージョンを確認する
$ node -v v12.14.1
- npm(パッケージマネージャ)のバージョンを確認する
$ npm -v 6.13.4以上です。
- 投稿日:2020-01-19T23:05:10+09:00
[Angular]module間でコンポーネントを共有する
概要
- Reactではコンポーネントはimportすればどこからでも使うことができるが、Angularでは一手間加えないとできなかったのでやり方のメモ
やり方
- 共通コンポーネント用のSharedModuleを作成してそれらを各moduleでimportすることで使えるようになる
サンプル
- 以下のようなhomeモジュールとaboutモジュールでCustomButtonコンポーネントを共有したい場面を想定して説明します
% tree src/app/ src/app/ ├── app-routing.module.ts ├── app.component.ts ├── app.module.ts ├── custom-button │ └── custom-button.component.ts ├── about │ ├── about-routing.module.ts │ ├── about.component.ts │ └── about.module.ts └── home ├── home-routing.module.ts ├── home.component.ts └── home.module.tsSharedModuleを作成
- 共有コンポーネントを管理する
SharedModule
を作成しますng generate module shared
- 生成された
shared.module.ts
に共有したいCustomButtonの定義を追加します
- declarationsとexportsの2か所に追加する必要があるので注意
- 共有したいコンポーネントを新しく作るときは同じように追加していくことになります
src/app/shared/shared.module.tsimport { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CustomButtonComponent } from '../custom-button/custom-button.component'; @NgModule({ declarations: [CustomButtonComponent], imports: [CommonModule], exports: [CustomButtonComponent], }) export class SharedModule {}AppModuleでSharedModuleを読み込む
- 作成したSharedModuleを
app.module.ts
で読み込むようにします
- importsに追加します
- 共有コンポーネント(この例ではCustomButton)の定義が入っている場合は消しておきます
src/app/app.module.tsimport { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { SharedModule } from './shared/shared.module'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule, AppRoutingModule, SharedModule], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
- ここまでが事前準備的なところです
共有コンポーネントを使いたいモジュールでSharedModuleを読み込む
- 今回の例でいうとhomeモジュールとaboutモジュールでCustomButtonを使いたいためそれぞれのmoduleでSharedModuleを読み込むようにします
- それぞれimportsに追加します
src/app/home/home.module.tsimport { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HomeRoutingModule } from './home-routing.module'; import { HomeComponent } from './home.component'; import { SharedModule } from '../shared/shared.module'; @NgModule({ declarations: [HomeComponent], imports: [CommonModule, HomeRoutingModule, SharedModule], }) export class HomeModule {}src/app/about/about.module.tsimport { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AboutRoutingModule } from './about-routing.module'; import { AboutComponent } from './about.component'; import { SharedModule } from '../shared/shared.module'; @NgModule({ declarations: [AboutComponent], imports: [CommonModule, AboutRoutingModule, SharedModule], }) export class AboutModule {}
- これで完成です!
- homeモジュールとaboutモジュール内で
<app-custom-button></app-custom-button>
が使えるようになりましたまとめ
- 共有したいコンポーネントはSharedModuleで管理してappモジュールと、コンポーネントを使いたいモジュールでimportすることで使い回せるようになりました
- appモジュールでもimportしないといけないのが個人的にはまりどころでした
参考ページ
- 投稿日:2020-01-19T23:04:53+09:00
Googleサービスでサーバーレスなカウントダウンサイトを作ってみた
完成イメージ
Googleアカウントがあれば、無料のサーバーレス運用ができます。
実際に作ったサイト
https://sites.google.com/view/olympic-paralympic/さっそく作成
Googleサイトを利用します。
https://sites.google.com/newテキストボックスを見出しに変更して、文字のバランスを整えます。
表題を編集します。
オリンピック・パラリンピックの文字の後ろで改行して、TOKYO2020を入力後スタイルを題名に変更、もう一つ改行して2020年7月24日 午後8時 開幕式まであとを入力後スタイルを小見出しに変更します。
※写真は次のサイトの素材を利用させていただきました。
フリー写真素材ぱくたそカウントダウン作成
カウントダウンをJavaScriptを使って作成します。
画面右側の挿入から埋め込みをクリックします。
ここに、htmlでコードを記入します。
一般的にはテキストエディタで作成して貼り付けるみたいです。<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width",initial-scale=1> <title>Countdown</title> <style type="text/css"> /*書式初期化*/ html,body,h1,h2,h3,h4,ul,ol,dl,li,dt,dd,p,div,span,img,a,table,tr,th,td { border: 0; font-size: 100%; font-weight: normal; margin: 0; padding: 0; } body { /*フォント設定*/ color:#FFFFFF; font-size: 300%; text-align: center; font-family: 'ヒラギノ角ゴシック Pro', 'Hiragino Kaku Gothic Pro', メイリオ, Meiryo, Osaka, 'MS Pゴシック', 'MS PGothic', sans-serif; } </style> </head> <body> <div id="countOutput"></div> <script LANGUAGE="JavaScript"> function dateCounter() { var timer = setInterval(function() { //現在の日時取得 var nowDate = new Date(); //カウントダウンしたい日を設定 var anyDate = new Date("2020/7/24 20:00:00"); //日数を計算 var daysBetween = Math.ceil((anyDate - nowDate)/(1000*60*60*24)-1); var ms = (anyDate - nowDate); if (ms >= 0) { //時間を取得 var h = Math.floor(ms / 3600000); var _h = h % 24; //分を取得 var m = Math.floor((ms - h * 3600000) / 60000); //秒を取得 var s = Math.round((ms - h * 3600000 - m * 60000) / 1000); //HTML上に出力 document.getElementById("countOutput").innerHTML = daysBetween + "日と" +_h + "時間" + m + "分" +s + "秒"; if ((h == 0) && (m == 0) && (s == 0)) { clearInterval(timer); document.getElementById("countOutput").innerHTML = "経過しました"; } }else{ document.getElementById("countOutput").innerHTML = "経過しました"; } }, 1000); } dateCounter(); </script> </body> </html>フォントが白のため、真っ白に見えます。
信じて挿入をクリックします。
下に挿入されるので、先程作成した表題の下にドラッグアンドドロップします。
カウントダウンのフィールドを選択し、縮めるなどして調整します。
出来たら、右上の公開ボタンをクリックします。
最後にウェブへの公開設定をします。
希望のウェブアドレスを入力し、サイトを閲覧できるユーザー等設定後、公開ボタンをクリックします。
公開すると、右上の公開の横にある下三角ボタンをクリックするとサイトを確認することができます。
おわりに
以上、Googleサイトを利用して、簡単にカウントダウンサイトを作る方法でした。
Googleアカウントがあれば、だれでもそのまま利用できるので、とても便利です。
JavaScriptで動かすので、ディスプレイに映し出してサイネージとしても利用できるかと思います。一時的にとか、ちょこっと的なときに、少しでも役立っていただけたら嬉しく思います。
- 投稿日:2020-01-19T22:53:45+09:00
webGLでVertex Animation Textureをやる
WebGLでVertex Animation Textureを実験的にやってみた記事です。
環境
・Three.js
・gsap
・Houdini Indie 18
Houdiniの無料版でできるかは確認していません。
・Windows 10Vertex Animation Texture について
VATとはそれぞれのピクセルに頂点の位置、回転などの情報を特定のフレーム分含めたテクスチャデータの事です。
テクスチャのサイズですが、ポイントの数、フレーム数が多くなるに連れてテクスチャサイズも大きくなります。
テクスチャのX軸が頂点番号になり、Y軸はフレーム数になります。HoudiniでVertex Animation Textureを出力する
https://www.sidefx.com/products/sidefx-labs/
sidefxlabsをインストールすることでLabs Vertex Animation Textureを使うことができます。
CGデータの出力
Labs Vertex Animation TextureでポリゴンデータをFBXで出力することが可能ですが、
FBXのデータ構造の扱いにくさや、カスタムのアトリビュートを出力してくれなかったり、頂点の出力がwebGLで扱うには少し面倒な形になってたので ROP GLTF EXPOTを使用して出力した、GLTF形式でCGデータを使用します。
ROP GLTF EXPOTを使用した場合、カスタムアトリビュートも一緒にエクスポートしてくれます。テクスチャデータについて
webGLで扱えるテクスチャの大きさは見る側の環境に依存します。
なので最大で4096pxにするのがいいと思います。
スマホとか考えると2048pxのほうがいいかもですが。
最大で4096pxとなると頂点の数は4096個以内に収めないといけません。
フレーム数も4096px以内になります。Labs Vertex Animation Textureで出力されつテクスチャデータは、
exr形式で32bitの画像ファイルになります。
web上でexr形式で32bitを扱うのは、調べたところできそうになかったので、
photoshopなどでpngなどに変換して使用します。
threejs内のライブラリでexrを扱うことができますが、うまく動かないことがあったので今回は除外します。
https://threejs.org/examples/webgl_loader_texture_exr.html
実際に作って読み込んでアニメーションさせてみる
ブラウザで表示させる
今回はテスト用にてっとり速く、確認できるデータを作成します。
boxを作成し、適当に色をつけて、ポイントの番号をidにするvexを書いて、
適当にアニメーションをつけたものをgltfで出力し、ブラウザで表示します。
gltfで出力するとき、タイムラインを一番最初にしておく必要がある
f@id = @ptnum;
ptnumそのままいれるとint型なのでエラーになります。Labs Vertex Animation Textureでtextureを出力させる
houdini outネットワークに入りLabs Vertex Animation Textureを使います。
今回がポジションをVATします。
一応カラーのVATも作成しておきます。設定は変更箇所は太文字になってます。
・user interface -normalに変更
・engineをMantraに変更
・sop pathにつかうオブジェクトを選択renderで出力させます。
出力させるとexportフォルダの中にmaterials、meshs,texturesファルダが作成されます。
使用するのはmaterials,texturesの2つです。
materialsフォルダの中に謎のjsonが入ってます。[ { "Name": "Soft", "doubleTex": 0, "height": 0.1, "normData": 1, "numOfFrames": 119, "packNorm": 1, "packPscale": 1, "padPowTwo": 0, "paddedSizeX": 0, "paddedSizeY": 0, "pivMax": 0.0, "pivMin": 0.0, "posMax": 200.0, "posMin": -200.0, "scaleMax": 0.0, "scaleMin": 0.0, "speed": 0.201680672269, "textureSizeX": 0, "textureSizeY": 0, "width": 0.1 } ]texturesフォルダの中にtextureが入ってます。
vertex_animation_textures1_col.exr
vertex_animation_textures1_pos.exr
これで準備完了です。
texture情報をもとに頂点を動かす
ここからコードを書いていきます。
デバックしながら書いてたので、かなり適当な変数名だったりしますが、そんなにコード量もないのでそのままコピペします。vertex.vsに
gl_Position = projectionMatrix * modelViewMatrix * vec4(vec3(testPos.r, testPos.b, testPos.g) + position, 1.0 );
とあります。
testPos.r, testPos.b, testPos.g
`の順番がおかしいのはhoudiniとwebglの座標系が違うからです。script.jsimport { BASE_DIR } from "../constants.yml"; import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; import vertex from "./shader/vertex.vs"; import fragment from "./shader/fragment.fs"; import { gsap } from "gsap"; async function loadTexture(url) { const load = new THREE.TextureLoader(); return new Promise((resolve, reject) => { load.load( `${BASE_DIR}model/${url}`, function (texture) { texture.generateMipmaps = false; texture.minFilter = THREE.LinearFilter; texture.magFilter = THREE.LinearFilter; const deta = { texture }; resolve(deta); } ); }); } async function loadConfigJson(url) { return new Promise((resolve, reject) => { fetch(`${BASE_DIR}model/${url}`) .then(function (response) { resolve(response.json()) }) }); } async function loadGLTF(url) { const load = new GLTFLoader(); return new Promise((resolve, reject) => { load.load(`${BASE_DIR}model/${url}`, function (object) { resolve(object); }); }); } const directory = 'cube'; async function init() { gsap.ticker.fps(24); let fps = 0; const json = await loadConfigJson(`${directory}/vertex_animation_textures1_data.json`); const jsonData = json[0] const colTexData = await loadTexture(`${directory}/vertex_animation_textures1_col.png`); const posTexData = await loadTexture(`${directory}/vertex_animation_textures1_pos.png`); const gltfData = await loadGLTF(`${directory}/output.glb`); const canvas = document.querySelector(".canvas"); const border = document.querySelector(".border"); const w = window.innerWidth; const h = window.innerHeight; const renderer = new THREE.WebGLRenderer({ canvas }); renderer.setSize(w, h); renderer.setPixelRatio(window.devicePixelRatio); renderer.setClearColor(0x000000); const camera = new THREE.PerspectiveCamera(60, w / h, 1, 1000); camera.position.z = 3; const scene = new THREE.Scene(); const light = new THREE.DirectionalLight(0xffffff); light.position.set(1, 10, 10); scene.add(light); renderer.render(scene, camera); let mesh; gltfData.scene.traverse(child => { if (child.isMesh) { mesh = child; console.log(mesh); const material = new THREE.ShaderMaterial({ wireframe: false, uniforms: { colorMap: { type: "t", value: colTexData.texture }, posMap: { type: "t", value: posTexData.texture }, totalNum: { type: "f", value: 7.0 }, totalFrame: { type: "f", value: jsonData.numOfFrames }, posMax: { type: "f", value: jsonData.posMax * 0.01 }, posMin: { type: "f", value: jsonData.posMin * 0.01 }, fps: { type: "f", value: fps } }, vertexShader: vertex, fragmentShader: fragment, side: THREE.DoubleSide }); child.castShadow = true; child.receiveShadow = true; child.material = material; } }); scene.add(gltfData.scene); var controls = new OrbitControls(camera, renderer.domElement); controls.update(); function animate() { gsap.ticker.add(animate); controls.update(); fps++; border.style.top = fps + 'px'; mesh.material.uniforms.fps.value = fps; renderer.render(scene, camera); console.log(fps, jsonData.numOfFrames); if (fps == jsonData.numOfFrames) fps = 0 } animate(); } init();vertex.vsprecision highp float; varying vec2 vUv; varying vec4 vColor; uniform sampler2D colorMap; uniform sampler2D posMap; uniform float posMax; uniform float posMin; attribute float _id; uniform float fps; uniform float totalNum; uniform float totalFrame; void main() { vUv = uv; float frag = 1.0 / totalNum; float range = posMax + (posMin * -1.0); float pu = frag * _id; // float test = 1.0; // float pv = 1.0 -fract(test/totalFrame); float pv = 1.0 -fract(fps/totalFrame); vec3 tPosition = texture2D(posMap,vec2(pu, pv)).rgb; vec3 calcPos = vec3(tPosition.r * range, tPosition.g * range, tPosition.b * range); vec3 testPos = vec3(posMin + calcPos.r, posMin + calcPos.g, posMin + calcPos.b); vec3 tColor = texture2D(colorMap, vec2(pu, pv)).rgb; vColor = vec4(tColor, 1.0); gl_Position = projectionMatrix * modelViewMatrix * vec4(vec3(testPos.r, testPos.b, testPos.g) + position, 1.0 ); }fragment.fsprecision highp float; varying vec2 vUv; varying vec4 vColor; void main() { vec4 color = vColor; gl_FragColor = color; }これで動かすことができます。
実際に見ると位置が開始が中心ではなく少しずれていると思います。
これはexrをphotoshopでbit数を下げてpngで出力すると何故か変に劣化するからです。1フレーム目は中心なので、アニメーションにもやりますが、今回はx軸に最大200,最小-200なので中心を正規化すると0.5になります。なので1フレームは0.5で灰色になるはずですが、photoshopでpngに変換することでなぜ値がずれます。
これを解決するには
https://convertio.co/ja/exr-png/
というサービスを利用してexrをpngに変換した場合、うまくいきます。
しかしこれを読み込ませてもずれます。
これはexrデータを16bit pngをさらに8bit pngにすると何故かうまくいきます。
そして8bit pngに変換するのにphotoshopを使います。これでうまくいきます。
ですが、32bitから8bitにしたことでかなり劣化します。
結果、頂点の位置がかなりぷるぷるします。
ズームするとよくわかります。
もともとゲーム開発などに使われる技術なので、web向けにするには
値を補完したり、pythonで独自にファイルを出力するコードをhoudiniに
書く必要がありそうです。大量の頂点を動かす
houdini上でclothの物理演算を行いそれをブラウザで再現して見ます。
こんな感じになります。
たぶん精度の問題で破綻してる部分がありますが、一応再現できていると思います。githubはこちら
https://github.com/machilda/houdini-to-three-vertex-animation
- 投稿日:2020-01-19T22:21:32+09:00
JavaScriptで動的に生成したテーブルをモーダルウインドウに表示
JavaScriptでJSONから動的に生成したテーブルをページング付きでモーダルウインドウに表示します。モーダルウインドウにランキング等を表示したい場合などに使ってください。
<!DOCTYPE html> <html lang="ja"> <head> <title>サンプル</title> <meta charset="utf-8"> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script> <style> /* モーダルのデザイン */ #modal-content{ display: none; width:50%; height: 80%; padding:20px 40px 40px 30px; border:2px solid #aaa; background:#fff; z-index:2; position:fixed; } #modal-overlay{ z-index:1; display:none; position:fixed; top:0; left:0; width:100%; height:120%; background-color:rgba(0,0,0,0.75); } #modal-close{ float: right; background: #C0C0C0; color: #FFF; width: 20px; height:20px; border-radius: 50%; text-align: center; overflow: hidden; transition: .4s; } #modal-body{ border: 1px solid gray; margin:3% 0%; height: 85%; overflow-y:scroll; overflow:hidden; } #modal-footer{ text-align: right; } /* ここより下は、モーダル内のテーブルデザイン */ th{ /* ヘッダ背景塗りつぶし */ background: #eee; } th,td { /* 枠線を1本線指定 */ border: solid 1px; width:auto; } table{ /* 枠線を1本線指定 */ border: solid 1px; border-collapse: collapse; white-space: nowrap;/*横スクロールに対してセルの幅を一定に保つ */ } </style> </head> <body> <p><a id="modal-open" class="button-link">モーダルウィンドウを開きます。</a></p> <!--モーダル画面--> <div id="modal-content"> <!--ヘッダー--> <div id="modal-header"> <span id="modal-title" >■ランキング</span> <button type="button" id="modal-close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <!--ボディー--> <div id="modal-body"> <!--テーブル生成位置--> <span id ='maintable'></span> </div> <!--フッター--> <div id="modal-footer"> <!--ページングボタン設置--> <button id="prevbtn" type="button"><</button> <span id="currentpage">currentpage</span> / <span id="lastpage">lastpage</span> <button id="nextbtn" type="button">></button> </div> </div> <script> // ページング機能の実装 jQuery(function($) { var page = 0; var displayrows = 3;// 1ページ当たり表示する行の数 // ページの表示 function draw() { $('#lastpage').html(Math.ceil(($('tr').size()-1)/displayrows)); $('#currentpage').html(page + 1); $('tr').hide(); $('tr:first,tr:gt(' + page * displayrows + '):lt(' + displayrows + ')').show();// 変数を使用する場合は' + + 'を忘れずに }; $('#prevbtn').click(function() {// 1ページ後進 if (page > 0) { page--; draw(); }; }); $('#nextbtn').click(function() {// 1ページ前進 if (page < ($('tr').size() - 1) /displayrows - 1) { page++; draw(); }; }); $("#modal-open").click( function(){ //キーボード操作などにより、オーバーレイが多重起動するのを防止する $(this).blur() ; //ボタンからフォーカスを外す if($("#modal-overlay")[0]) return false ; //新しくモーダルウィンドウを起動しない [下とどちらか選択] //if($("#modal-overlay")[0]) $("#modal-overlay").remove() ; //現在のモーダルウィンドウを削除して新しく起動する [上とどちらか選択] //オーバーレイ用のHTMLコードを、[body]内の最後に生成する $("body").append('<div id="modal-overlay"></div>'); //[$modal-overlay]をフェードインさせる $("#modal-overlay").fadeIn("slow"); //[$modal-content]をフェードインさせる $("#modal-content").fadeIn("slow"); } ); $("#modal-overlay,#modal-close").unbind().click(function(){ //[#modal-overlay]と[#modal-close]をフェードアウトする $("#modal-content,#modal-overlay").fadeOut("slow",function(){ //フェードアウト後、[#modal-overlay]をHTML(DOM)上から削除 $("#modal-overlay").remove(); }); }); //センタリングをする関数 function centeringModalSyncer(){ //画面(ウィンドウ)の幅を取得し、変数[w]に格納 var w = $(window).width(); //画面(ウィンドウ)の高さを取得し、変数[h]に格納 var h = $(window).height(); //コンテンツ(#modal-content)の幅を取得し、変数[cw]に格納 var cw = $("#modal-content").outerWidth({margin:true}); //コンテンツ(#modal-content)の高さを取得し、変数[ch]に格納 var ch = $("#modal-content").outerHeight({margin:true}); //コンテンツ(#modal-content)を真ん中に配置するのに、左端から何ピクセル離せばいいか?を計算して、変数[pxleft]に格納 var pxleft = ((w - cw)/2); //コンテンツ(#modal-content)を真ん中に配置するのに、上部から何ピクセル離せばいいか?を計算して、変数[pxtop]に格納 var pxtop = ((h - ch)/2); //[#modal-content]のCSSに[left]の値(pxleft)を設定 $("#modal-content").css({"left": pxleft + "px"}); //[#modal-content]のCSSに[top]の値(pxtop)を設定 $("#modal-content").css({"top": pxtop + "px"}); }; centeringModalSyncer();//センタリングの実施 // jsonデータを基にtable要素を生成 var table = document.createElement('table'); // ヘッダーを作成 var tr = document.createElement('tr'); for (key in json[0]) { // th要素を生成 var th = document.createElement('th'); // th要素内にテキストを追加 th.textContent = key; // th要素をtr要素の子要素に追加 tr.appendChild(th); } // tr要素をtable要素の子要素に追加 table.appendChild(tr); // テーブル本体を作成 for (var i = 0; i < json.length; i++) { // tr要素を生成 var tr = document.createElement('tr'); // th・td部分のループ for (key in json[0]) { // td要素を生成 var td = document.createElement('td'); // td要素内にテキストを追加 td.textContent = json[i][key]; // td要素をtr要素の子要素に追加 tr.appendChild(td); } // tr要素をtable要素の子要素に追加 table.appendChild(tr); } // 生成したtable要素を追加する document.getElementById('maintable').appendChild(table); //table要素を生成した直後の初回表示 draw(); }); </script> <script> var json =[ //jsonサンプルデータ { "順位":1 ,"氏名":"王貞治" , "本数":868 } , { "順位":2 ,"氏名":"野村克也" ,"本数":657 } , { "順位":3 ,"氏名":"門田博光" ,"本数":567 } , { "順位":4 ,"氏名":"山本浩二" ,"本数":536 } , { "順位":5 ,"氏名":"清原和博" ,"本数":525 } ] </script> </body>
- 投稿日:2020-01-19T22:21:32+09:00
JavaScriptでテーブルをモーダルウインドウに表示
JavaScriptでJSONから動的に生成したテーブルをページング付きでモーダルウインドウに表示します。モーダルウインドウにランキング等を表示したい場合などに使ってください。
<!DOCTYPE html> <html lang="ja"> <head> <title>サンプル</title> <meta charset="utf-8"> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script> <style> /* モーダルのデザイン */ #modal-content{ display: none; width:50%; height: 80%; padding:20px 40px 40px 30px; border:2px solid #aaa; background:#fff; z-index:2; position:fixed; } #modal-overlay{ z-index:1; display:none; position:fixed; top:0; left:0; width:100%; height:120%; background-color:rgba(0,0,0,0.75); } #modal-close{ float: right; background: #C0C0C0; color: #FFF; width: 20px; height:20px; border-radius: 50%; text-align: center; overflow: hidden; transition: .4s; } #modal-body{ border: 1px solid gray; margin:3% 0%; height: 85%; overflow-y:scroll; overflow:hidden; } #modal-footer{ text-align: right; } /* ここより下は、モーダル内のテーブルデザイン */ th{ /* ヘッダ背景塗りつぶし */ background: #eee; } th,td { /* 枠線を1本線指定 */ border: solid 1px; width:auto; } table{ /* 枠線を1本線指定 */ border: solid 1px; border-collapse: collapse; white-space: nowrap;/*横スクロールに対してセルの幅を一定に保つ */ } </style> </head> <body> <p><a id="modal-open" class="button-link">モーダルウィンドウを開きます。</a></p> <!--モーダル画面--> <div id="modal-content"> <!--ヘッダー--> <div id="modal-header"> <span id="modal-title" >■ランキング</span> <button type="button" id="modal-close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <!--ボディー--> <div id="modal-body"> <!--テーブル生成位置--> <span id ='maintable'></span> </div> <!--フッター--> <div id="modal-footer"> <!--ページングボタン設置--> <button id="prevbtn" type="button"><</button> <span id="currentpage">currentpage</span> / <span id="lastpage">lastpage</span> <button id="nextbtn" type="button">></button> </div> </div> <script> // ページング機能の実装 jQuery(function($) { var page = 0; var displayrows = 3;// 1ページ当たり表示する行の数 // ページの表示 function draw() { $('#lastpage').html(Math.ceil(($('tr').size()-1)/displayrows)); $('#currentpage').html(page + 1); $('tr').hide(); $('tr:first,tr:gt(' + page * displayrows + '):lt(' + displayrows + ')').show();// 変数を使用する場合は' + + 'を忘れずに }; $('#prevbtn').click(function() {// 1ページ後進 if (page > 0) { page--; draw(); }; }); $('#nextbtn').click(function() {// 1ページ前進 if (page < ($('tr').size() - 1) /displayrows - 1) { page++; draw(); }; }); $("#modal-open").click( function(){ //キーボード操作などにより、オーバーレイが多重起動するのを防止する $(this).blur() ; //ボタンからフォーカスを外す if($("#modal-overlay")[0]) return false ; //新しくモーダルウィンドウを起動しない [下とどちらか選択] //if($("#modal-overlay")[0]) $("#modal-overlay").remove() ; //現在のモーダルウィンドウを削除して新しく起動する [上とどちらか選択] //オーバーレイ用のHTMLコードを、[body]内の最後に生成する $("body").append('<div id="modal-overlay"></div>'); //[$modal-overlay]をフェードインさせる $("#modal-overlay").fadeIn("slow"); //[$modal-content]をフェードインさせる $("#modal-content").fadeIn("slow"); } ); $("#modal-overlay,#modal-close").unbind().click(function(){ //[#modal-overlay]と[#modal-close]をフェードアウトする $("#modal-content,#modal-overlay").fadeOut("slow",function(){ //フェードアウト後、[#modal-overlay]をHTML(DOM)上から削除 $("#modal-overlay").remove(); }); }); //センタリングをする関数 function centeringModalSyncer(){ //画面(ウィンドウ)の幅を取得し、変数[w]に格納 var w = $(window).width(); //画面(ウィンドウ)の高さを取得し、変数[h]に格納 var h = $(window).height(); //コンテンツ(#modal-content)の幅を取得し、変数[cw]に格納 var cw = $("#modal-content").outerWidth({margin:true}); //コンテンツ(#modal-content)の高さを取得し、変数[ch]に格納 var ch = $("#modal-content").outerHeight({margin:true}); //コンテンツ(#modal-content)を真ん中に配置するのに、左端から何ピクセル離せばいいか?を計算して、変数[pxleft]に格納 var pxleft = ((w - cw)/2); //コンテンツ(#modal-content)を真ん中に配置するのに、上部から何ピクセル離せばいいか?を計算して、変数[pxtop]に格納 var pxtop = ((h - ch)/2); //[#modal-content]のCSSに[left]の値(pxleft)を設定 $("#modal-content").css({"left": pxleft + "px"}); //[#modal-content]のCSSに[top]の値(pxtop)を設定 $("#modal-content").css({"top": pxtop + "px"}); }; centeringModalSyncer();//センタリングの実施 // jsonデータを基にtable要素を生成 var table = document.createElement('table'); // ヘッダーを作成 var tr = document.createElement('tr'); for (key in json[0]) { // th要素を生成 var th = document.createElement('th'); // th要素内にテキストを追加 th.textContent = key; // th要素をtr要素の子要素に追加 tr.appendChild(th); } // tr要素をtable要素の子要素に追加 table.appendChild(tr); // テーブル本体を作成 for (var i = 0; i < json.length; i++) { // tr要素を生成 var tr = document.createElement('tr'); // th・td部分のループ for (key in json[0]) { // td要素を生成 var td = document.createElement('td'); // td要素内にテキストを追加 td.textContent = json[i][key]; // td要素をtr要素の子要素に追加 tr.appendChild(td); } // tr要素をtable要素の子要素に追加 table.appendChild(tr); } // 生成したtable要素を追加する document.getElementById('maintable').appendChild(table); //table要素を生成した直後の初回表示 draw(); }); </script> <script> var json =[ //jsonサンプルデータ { "順位":1 ,"氏名":"王貞治" , "本数":868 } , { "順位":2 ,"氏名":"野村克也" ,"本数":657 } , { "順位":3 ,"氏名":"門田博光" ,"本数":567 } , { "順位":4 ,"氏名":"山本浩二" ,"本数":536 } , { "順位":5 ,"氏名":"清原和博" ,"本数":525 } ] </script> </body> </html>
- 投稿日:2020-01-19T22:21:32+09:00
JavaScriptでテーブルを・・・
JavaScriptでJSONから動的に生成したテーブルをページング付きでモーダルウインドウに表示します。モーダルウインドウにランキング等を表示したい場合などに使ってください。
<!DOCTYPE html> <html lang="ja"> <head> <title>サンプル</title> <meta charset="utf-8"> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script> <style> /* モーダルのデザイン */ #modal-content{ display: none; width:50%; height: 80%; padding:20px 40px 40px 30px; border:2px solid #aaa; background:#fff; z-index:2; position:fixed; } #modal-overlay{ z-index:1; display:none; position:fixed; top:0; left:0; width:100%; height:120%; background-color:rgba(0,0,0,0.75); } #modal-close{ float: right; background: #fff; color: #C0C0C0; width: 20px; height:20px; border-radius: 50%; text-align: center; overflow: auto; transition: .4s; } #modal-body{ border: 1px solid gray; margin:3% 0%; height: 85%; overflow-y:scroll; overflow:hidden; } #modal-footer{ text-align: right; } /* ここより下は、モーダル内のテーブルデザイン */ th{ /* ヘッダ背景塗りつぶし */ background: #eee; } th,td { /* 枠線を1本線指定 */ border: solid 1px; width:auto; } table{ /* 枠線を1本線指定 */ border: solid 1px; border-collapse: collapse; white-space: nowrap;/*横スクロールに対してセルの幅を一定に保つ */ } </style> </head> <body> <p><a id="modal-open" class="button-link">モーダルウィンドウを開きます。</a></p> <!--モーダル画面--> <div id="modal-content"> <!--ヘッダー--> <div id="modal-header"> <span id="modal-title" >■ランキング</span> <a type="button" id="modal-close" data-dismiss="modal" aria-label="Close">✖︎</a> </div> <!--ボディー--> <div id="modal-body"> <!--テーブル生成位置--> <span id ='maintable'></span> </div> <!--フッター--> <div id="modal-footer"> <!--ページングボタン設置--> <button id="prevbtn" type="button"><</button> <span id="currentpage">currentpage</span> / <span id="lastpage">lastpage</span> <button id="nextbtn" type="button">></button> </div> </div> <script> // ページング機能の実装 jQuery(function($) { var page = 0; var displayrows = 3;// 1ページ当たり表示する行の数 // ページの表示 function draw() { $('#lastpage').html(Math.ceil(($('tr').size()-1)/displayrows)); $('#currentpage').html(page + 1); $('tr').hide(); $('tr:first,tr:gt(' + page * displayrows + '):lt(' + displayrows + ')').show();// 変数を使用する場合は' + + 'を忘れずに }; $('#prevbtn').click(function() {// 1ページ後進 if (page > 0) { page--; draw(); }; }); $('#nextbtn').click(function() {// 1ページ前進 if (page < ($('tr').size() - 1) /displayrows - 1) { page++; draw(); }; }); $("#modal-open").click( function(){ //キーボード操作などにより、オーバーレイが多重起動するのを防止する $(this).blur() ; //ボタンからフォーカスを外す if($("#modal-overlay")[0]) return false ; //新しくモーダルウィンドウを起動しない [下とどちらか選択] //if($("#modal-overlay")[0]) $("#modal-overlay").remove() ; //現在のモーダルウィンドウを削除して新しく起動する [上とどちらか選択] //オーバーレイ用のHTMLコードを、[body]内の最後に生成する $("body").append('<div id="modal-overlay"></div>'); //[$modal-overlay]をフェードインさせる $("#modal-overlay").fadeIn("slow"); //[$modal-content]をフェードインさせる $("#modal-content").fadeIn("slow"); } ); $("#modal-overlay,#modal-close").unbind().click(function(){ //[#modal-overlay]と[#modal-close]をフェードアウトする $("#modal-content,#modal-overlay").fadeOut("slow",function(){ //フェードアウト後、[#modal-overlay]をHTML(DOM)上から削除 $("#modal-overlay").remove(); }); }); //センタリングをする関数 function centeringModalSyncer(){ //画面(ウィンドウ)の幅を取得し、変数[w]に格納 var w = $(window).width(); //画面(ウィンドウ)の高さを取得し、変数[h]に格納 var h = $(window).height(); //コンテンツ(#modal-content)の幅を取得し、変数[cw]に格納 var cw = $("#modal-content").outerWidth({margin:true}); //コンテンツ(#modal-content)の高さを取得し、変数[ch]に格納 var ch = $("#modal-content").outerHeight({margin:true}); //コンテンツ(#modal-content)を真ん中に配置するのに、左端から何ピクセル離せばいいか?を計算して、変数[pxleft]に格納 var pxleft = ((w - cw)/2); //コンテンツ(#modal-content)を真ん中に配置するのに、上部から何ピクセル離せばいいか?を計算して、変数[pxtop]に格納 var pxtop = ((h - ch)/2); //[#modal-content]のCSSに[left]の値(pxleft)を設定 $("#modal-content").css({"left": pxleft + "px"}); //[#modal-content]のCSSに[top]の値(pxtop)を設定 $("#modal-content").css({"top": pxtop + "px"}); }; centeringModalSyncer();//センタリングの実施 // jsonデータを基にtable要素を生成 var table = document.createElement('table'); // ヘッダーを作成 var tr = document.createElement('tr'); for (key in json[0]) { // th要素を生成 var th = document.createElement('th'); // th要素内にテキストを追加 th.textContent = key; // th要素をtr要素の子要素に追加 tr.appendChild(th); } // tr要素をtable要素の子要素に追加 table.appendChild(tr); // テーブル本体を作成 for (var i = 0; i < json.length; i++) { // tr要素を生成 var tr = document.createElement('tr'); // th・td部分のループ for (key in json[0]) { // td要素を生成 var td = document.createElement('td'); // td要素内にテキストを追加 td.textContent = json[i][key]; // td要素をtr要素の子要素に追加 tr.appendChild(td); } // tr要素をtable要素の子要素に追加 table.appendChild(tr); } // 生成したtable要素を追加する document.getElementById('maintable').appendChild(table); //table要素を生成した直後の初回表示 draw(); }); </script> <script> var json =[ //jsonサンプルデータ { "順位":1 ,"氏名":"王貞治" , "本数":868 } , { "順位":2 ,"氏名":"野村克也" ,"本数":657 } , { "順位":3 ,"氏名":"門田博光" ,"本数":567 } , { "順位":4 ,"氏名":"山本浩二" ,"本数":536 } , { "順位":5 ,"氏名":"清原和博" ,"本数":525 } ] </script> </body> </html>
- 投稿日:2020-01-19T22:06:44+09:00
エンジニアのポートフォリオを作ってみた
以前からポートフォリオサイトを作ってみたかったので今回作ってみました。
https://sssttt-maker.github.io/portfolio/
github.ioで公開できる最近まで知りませんでした。
デザイン
デザインは、僕の大好きなブランド会社のTOKYO BASEさんのホームページを参考にさせていただきました。
http://www.tokyobase.co.jp/パララックスサイトみたいなのがかっこいいなと思ったので導入。
セクションが次々に動いていてとても気持ちいいです。cssで
position: sticky;というのをsection指定にして入れるだけで簡単に再現できちゃいます。
- 投稿日:2020-01-19T21:43:06+09:00
JavaScript勉強の記録その21:setIntervalとclerIntervalを使って関数の呼び出し
setIntervalを使って関数を呼び出す
setInterval(関数名,ミリ秒)とすることで、特定のメソッドを指定したミリ秒間隔で呼び続けることができます。
以下の例では、showDateTimePerSecondという関数を1秒間隔で呼び、Dateオブジェクトを生成し、コンソールに表示しています。index.jsfunction showDateTimePerSecond() { console.log(new Date()); }; setInterval(showDateTimePerSecond,1000) //=>Sun Jan 19 2020 21:27:11 GMT+0900 (日本標準時) //=>Sun Jan 19 2020 21:27:12 GMT+0900 (日本標準時) //=>.....clearIntervalを使ってsetIntervalを止める
反対にclearIntervalを利用すれば、setIntervalで呼び続ける処理を止めることができます。
以下の例では変数iが3になった時点で、clearIntervalを呼んで、処理を止めています。index.jslet i = 0; function showDateTimePerSecond() { console.log(new Date()); i++ if (i > 2) { clearInterval(intervalId); } }; const intervalId = setInterval(showDateTimePerSecond,1000); console.log(intervalId); //=>1 //=>Sun Jan 19 2020 21:36:05 GMT+0900 (日本標準時) //=>Sun Jan 19 2020 21:36:06 GMT+0900 (日本標準時) //=>Sun Jan 19 2020 21:36:07 GMT+0900 (日本標準時)clearIntervalは引数にsetIntervalの返り値を渡してあげることによって、どのsetIntervalを止めるか指定することができます。
上記の例ではあえて、console.log(intervalId)として、setInterval()の返り値を表示させています。
つまり上記の例では返り値が1のsetIntervalを止めているということです。
- 投稿日:2020-01-19T21:25:26+09:00
【Vue.JS】 WebPackを使わずにコンポーネントで遊ぶ
対象読者
- 1. 環境構築はやりたくないけど、すぐにVueで遊んでみたい方
- .vueファイルを使用する場合、WebPackサーバーなどの環境構築が必要となる
- 2. 自分のPC上でとりあえず遊んでみたい方
- ※ サイトとして公開する場合は環境構築が必要
サンプルアプリ
手法
- .vueファイルを使わずに、全て.jsファイルで定義する
import
文 ,export
文を使わずにコンポーネントを使用する
- 通常のJavaScriptでimport文を使用するとエラーとなるため
作り方
フォルダ構成
- 空のフォルダを作成し、その中に以下の4ファイルを作成する
- フォルダ名は任意。ここでは sample_folder とした
フォルダ構成sample_folder ┝ application.html ┝ application.js ┝ parent_vue.js ← 自作のコンポーネントを記述する ┝ vue.js ← CDNをダウンロードする(後述)ファイル生成方法
- 下記のソースコードをファイルごとに丸コピすれば動作します
- 解説を見たい方
- 各ソースコードの下に解説がございます
1. HTMLファイル
application.html<!DOCTYPE html> <html lang="js"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>SampleApp</title> </head> <body> <div id="app"> <!-- ② このdiv内で自作コンポーネントを呼び出している --> <parent-component></parent-component> </div> </body> <!-- ① 同一フォルダ内のファイルをインポートする。--> <script src="vue.js"></script> <script src="parent_vue.js"></script> <script src="application.js"></script> </html>
①
<script src="○○.js"></script>
の部分
- この部分で同じフォルダ内の.jsファイルを全て読み込む
② 自作したコンポーネントを呼び出すために、
<div id="app"></div>
タグを作成する
- このdivの外では、vueコンポーネントを使用できないことに注意
- id名は何でも良いが、後述するapplication.js内で定義するid名と同名とする
<parent-component></parent-component>
が自作したコンポーネントタグである
- 上記のタグ名は何でも良いが、後述するparent_vue.js内で定義するコンポーネント名と同名でなければならない
2. JSファイル(コンポーネント呼び出し用)
application.jsvar parent = new Vue({ el: "#app", // application.htmlで定義したid名と同一にする });
- このファイルでVueインスタンスを生成する
el:
には、application.htmlで定義したid名を書く3. JSファイル(コンポーネント定義用)
parent_vue.js// Vue.component('コンポーネント名', {定義}) の形式とする Vue.component('parent-component',{ template: ` <div id="parent_root"> <div v-bind:style="myStyle" v-on:click="changeColor"> {{message}} </div> </div> `, // component内ではdataは関数として定義しなければならない data: function() { return { message: "Click! Me.", myStyle: { color: 'red', fontSize: '18px', "background-color": '#ebb', border: 'solid 2px', } } }, methods: { changeColor: function(){ console.log("checked."); this.message ='confirmed'; this.$set(this.myStyle, 'color', 'green'); this.$set(this.myStyle, 'color', 'green'); this.$set(this.myStyle, 'background-color', '#beb'); } }, });
- このファイルでは、自作のコンポーネントを定義する
Vue.component('コンポーネント名', {定義})
の形式とし、{ }
内に記述する- コンポーネント名は、application.htmlで使用するコンポーネントタグと同名でなければならない
補足: templateの書き方(規則)
template:
部分とは
- htmlに相当する部分。下記2点を守ること
- 1. template内では、初めと終わりを
`
バッククォーテーションで囲うこと- 2. template内では、必ず単一の
<div>タグ
で囲うこと参考)templateの書き方template: `<div> この中に、htmlタグを自由に記述する </div>`,補足: Style(CSSに相当)の書き方
- 今回の書き方では
<style>タグ
が使用できないため、代わりに2つの手法を紹介する
- 1.
v-bind:style
を使用し、自作のスタイルをdata:
内で定義する
- 本記事ではこの方法を採用。
myStyle
として定義している- 2.
is属性
を使用して、styleタグを偽装する
- template内に一旦別名のタグを作成し、is属性でstyleタグとして機能させる
- 方法はこちら
4. CDNファイル
- Vue公式のCDNダウンロードサイトを開き、
直接組み込み
の章までスクロールする開発バージョン
ボタンをクリックし、ファイルをダウンロード- ダウンロードした
vue.js
ファイルを上記のsample_folder内にコピーすれば準備完了
- (記事上部の
フォルダ構成
の章を参照のこと)- 簡単!
動作
- 作成した
application.html
をダブルクリックすれば、ページが表示されます参考
- Vue.JS入門
- たにぐちまこと氏の "ともすたチャンネル" (Youtube)
- 初心者にもわかりやすく説明されているため、オススメです。
- 投稿日:2020-01-19T20:21:31+09:00
IE11でURLSearchParamsがエラーになる
エラー
Vue.jsで開発中に、axiosでPOSTするのに
URLSearchParams
を使ったらIE11でエラーが出てしまった。[Vue warn]: Error in v-on handler (Promise/async): "ReferenceError: 'URLSearchParams' は定義されていません。"axios
こんな感じで使っていた。
const params = new URLSearchParams(); params.append("hoge", "hoge"); axios .post(url, params) .then(res => { return res; }) .catch(error => { return error; });URLSearchParams
URLSearchParamsはそもそもIE11では使えないものらしい。
https://developer.mozilla.org/en-US/docs/Web/API/URL#Browser_compatibilityurl-search-params-polyfill
以下のpolyfillを追加して解消した。
jerrybendy/url-search-params-polyfill: a simple polyfill for javascript URLSearchParams追加
npm i --save url-search-params-polyfill
main.jsimport "url-search-params-polyfill";参考
IE11 > axios > POSTのつもりがうまく送れてないとき
IE11でURLSearchParamsを使った処理でエラーが発生する
- 投稿日:2020-01-19T20:18:45+09:00
Javascript -dotinstall object
forEach
{ const scores = [80,90,40,70]; scores.forEach((score,index)=>{ console.log(`Score index:${index}: ${score}`); }) }Score index:0: 80 Score index:1: 90 Score index:2: 40 Score index:3: 70map
{ const prices = [100,200,300]; const updatePrices = prices.map((price)=>{ return price + 20; }); console.log(updatePrices); }配列に、何らかの処理を加えたい場合
(3) [120, 220, 320]省略記法
・引数が一つの場合、(引数)の()を省略できる。
・returnのみ一行の場合は、return{}も省略できる。{ const prices = [100,200,300]; const updatePrices = prices.map(price => price + 20); console.log(updatePrices); }filter
{ const numbers = [1,4,7,8,10]; const evenNumbers = numbers.filter(number =>{ if(number % 2 ===0){ return true; }else{ return false; } }); console.log(evenNumbers); }(3) [4, 8, 10]省略形
{ const numbers = [1,4,7,8,10]; const evenNumbers = numbers.filter(number => number % 2 === 0); console.log(evenNumbers); }
- 投稿日:2020-01-19T19:57:49+09:00
JavaScript-dotinstall オブジェクト①
オブジェクト
配列
配列の生成
{ const scores = [80,90,100] console.log(scores); }結果
Array(3) 0: 80 1: 90 2: 100配列へのアクセスと要素の変更
{ const scores = [80,90,100] console.log(scores); console.log(scores[0]);//変更前 scores[0] = 1; console.log(scores[0]);//変更後 console.log(scores.length);//要素の数 }(3) [80, 90, 100] 80 1 3** constは scores = 10;などと値の再代入はできないが、要素の変更はできる。
配列の反復処理
{ const scores = [80,90,100]; // console.log(`Score; ${scores[0]}`) // console.log(`Score; ${scores[1]}`) // console.log(`Score; ${scores[2]}`) //for文で書き換える for(let i = 0; i < scores.length; i++){ console.log(`Score: ${scores[i]}`); } }Score: 80 Score: 90 Score: 100配列要素の追加・削除
- unshift 先頭に追加(a,b);
- shift 末尾から削除();
- push 末尾に追加(a,b);
- pop 末尾から削除();
{ const scores = [80,90,100]; //先頭追加unshift / 削除shift scores.push(60,50); //80,90,100,60,50 scores.shift(); //90,100,60,50 //末尾追加push / 削除 pop for(let i = 0; i < scores.length; i++){ console.log(`Score ${i}:${scores[i]}`); } }Score: 90 Score: 100 Score: 60 Score: 50配列要素の変更
spriceを使う a.sprice(変化が始まる位置(index),削除数,追加したい要素);
{ const scores = [80,90,100]; scores.splice(1,0,40,50); //90(index = 1 )の次に40,50を挿入。削除数は0 for(let i = 0; i < scores.length; i++){ console.log(`Score ${i}:${scores[i]}`); } }Score 0:80 Score 1:40 Score 2:50 Score 3:90 Score 4:100スプレッド演算子
{ const otherScores = [10,20]; // const scores = [80,90,100]; //otherScoresをscoresに入れたい! //scores = [80,90,100,otherScores]としてしまうと、 //80,90,100,Array[2]の形になってしまうので、スプレッド構文を使う。 const scores = [80,90,100,...otherScores]; console.log(scores); }(5) [80, 90, 100, 10, 20]引数に展開
function sum(a,b){ console.log(a+b); } sum(...otherScores); //sum(10,20)と同義30分割代入
{ const scores = [80,90,40,70]; const [a,b,c,d] = scores; console.log(a); console.log(b); console.log(c); console.log(d); }scoresの中身80,90,40,70を一回の記述で代入できる。
80 90 40 70分割代入しないパターン
const scores = [80,90,40,70]; const a = scores[0]; const b = scores[1]; const c = scores[2]; const d = scores[3]; console.log(a); console.log(b); console.log(c); console.log(d);
- 投稿日:2020-01-19T19:54:22+09:00
javascriptでmp3ファイルを再生
HTMLを編集
<button id="btn">再生</button> <button id="btn2">停止</button> <audio id="audio"></audio>これらを追加する
javascriptを書く
HTMLの中にscriptタグをかいてHTMLにjavascriptを書くか、
javascriptファイルを作ってそれでjavascriptを書くか。
今回はめんどくさいのでHTMLのなかにjavascriptを書きます。 headタグ内に書いてみてください
。 エラーでたら、bodyタグ内に書いてみましょう。<script> var btn = document.getElementById("btn"); //document.getElementByIdを使ってHTMLのbtnというIDを取得 var btn2 = document.getElementById("btn2");//document.getElementByIdを使ってHTMLのbtn2というIDを取得 btn.addEventListener('click', function(){ //もしbtn(再生)をクリックしたら.. audio.src='ここにmp3ファイル名を書く'; audio.play(); //audioを再生 }); btn.addEventListener('click', function(){ audio.pause(); //audioを止める audio.currentTime = 0; //時間を0に }); </script>これで再生と停止ができたみたいです!
- 投稿日:2020-01-19T19:50:57+09:00
Amazon Prime VideoやAbema TVなどを画面端の小さいウィンドウで再生する方法
javascript:document.querySelector("video[src]").requestPictureInPicture();をブックマークのURLに追加。
動画を再生してるときにブックマークを開く。
参考
- 投稿日:2020-01-19T19:04:46+09:00
micro:bitとブラウザ間で文字列をやり取りするまでの手順
目的
スマホ等からWebBluetoothを使って micro:bit に文字列を送信、
また、逆に micro:bit からスマホ等に文字列を送信するための手順をまとめています。
使用するもの
- micro:bit
- USB ケーブル
- WebBluetooth 対応ブラウザ(2020年1月時点でPCまたはAndroidのChrome)
- テキストエディタ
micro:bit 側のプログラム作成
まずは Bluetooth に接続、切断されたことがわかるようにしてみます。
https://makecode.microbit.org にアクセスします。
右上の歯車をクリックし、プロジェクトの設定を選択します。
※日本語表示になっていない場合は、Language から、日本語を選択します。
名前を UART にし、No Pairing Required: Anyone can connect via Bluetooth. を有効にします。
※これで WebBluetoothから面倒な手続きなしで micro:bit に接続できるようになります。
「一部の拡張機能を削除してbluetoothを追加する」ボタンを押します。
bluetooth - その他 のなかの「Bluetooth UARTサービス」を「最初だけ」ブロックの中に配置します。
接続されていない状態がわかるように、基本 - アイコン から目を閉じた顔マークを配置します。
Bluetooth が接続されたことがわかるように、Bluetooth のなかの 「Bluetooth 接続されたとき」と笑った顔マークを配置します。
Bluetooth が切断されたことがわかるように、「Bluetooth 接続が切断されたとき」と目を閉じた顔マークを配置します。(顔マークは先に配置したものを右クリックで複製すると早いです。)
作成したプログラムの書き込み
次のようなポップアップが表示されるので、 micro:bit を選択して、「接続」ボタンを押します。
※これで、画面左下の「ダウンロード」ボタンを押すと、作成したプログラムが直接 micro:bit に書き込まれるようになります。
画面左下の「ダウンロード」ボタンを押し、プログラムを書き込みます。
書き込みが成功すれば microbit に顔マークが表示されます。
ブラウザ側のプログラム作成
テキストエディタを起動して、次の内容を入力します。
<!DOCTYPE html> <meta charset="UTF-8"> <title>Micro:bit UART 通信</title> <style type="text/css"> * { font-size: 3vmin; padding: 3vmin; } body { text-align: center; } </style> <h1>Micro:bit UART 通信</h1> <form> <button type="button" id="connect">接続</button> </form> <script> const UUID_UART_SERVICE = '6e400001-b5a3-f393-e0a9-e50e24dcca9e' let gatt = null const update = connected => { document.getElementById('connect').textContent = connected ? '切断' : '接続' } document.getElementById('connect').addEventListener('click', e => { if(!(navigator.bluetooth && navigator.bluetooth.requestDevice)) { alert('WebBluetooth に未対応のブラウザです。') return } if (document.getElementById('connect').textContent == '接続') { navigator.bluetooth.requestDevice({ filters: [ { services: [UUID_UART_SERVICE] }, { namePrefix: 'BBC micro:bit' } ] }).then(device => { gatt = device.gatt return gatt.connect() }).then(server => { update(true) }).catch(function(err) { alert(err) }) } else { gatt.disconnect() update(false) } }) </script>
navigator.bluetooth.requestDevice で micro:bit を探します。
device.gatt が通信用のハンドルのようなものです。
gatt.connect で接続します。
接続が成功したら、ボタンを切断に変えるようにしています。
切断するのは gatt.disconnect です。
Bluetooth 接続の動作確認
作成したファイルを uart1.html という名前で保存し、ブラウザで開きます。
「接続」ボタンを押すと、次のようなポップアップが表示されるので、 micro:bit を選択して、「ペア設定」ボタンを押します。
うまく行けば、micro:bit の顔の表示が変わり、ブラウザのボタンは切断ボタンに変わります。
micro:bit からブラウザへ文字を送信する
ブラウザ側のプログラム変更
下記内容で uart2.html ファイルを作成し、ブラウザで開きます。
<!DOCTYPE html> <meta charset="UTF-8"> <title>Micro:bit UART 通信</title> <style type="text/css"> * { font-size: 3vmin; padding: 3vmin; } body { text-align: center; } textarea { vertical-align: middle; } </style> <h1>Micro:bit UART 通信</h1> <form> <div> <button type="button" id="connect">接続</button> 受信内容<textarea id="recieve_text"></textarea> </div> </form> <script> const UUID_UART_SERVICE = '6e400001-b5a3-f393-e0a9-e50e24dcca9e' const UUID_TX_CHAR_CHARACTERISTIC = '6e400002-b5a3-f393-e0a9-e50e24dcca9e' let gatt = null let tx = null const update = connected => { document.getElementById('connect').textContent = connected ? '切断' : '接続' } document.getElementById('connect').addEventListener('click', e => { if(!(navigator.bluetooth && navigator.bluetooth.requestDevice)) { alert('WebBluetooth に未対応のブラウザです。') return } if (document.getElementById('connect').textContent == '接続') { navigator.bluetooth.requestDevice({ filters: [ { services: [UUID_UART_SERVICE] }, { namePrefix: 'BBC micro:bit' } ] }).then(device => { gatt = device.gatt return gatt.connect() }).then(server => server.getPrimaryService(UUID_UART_SERVICE) ).then(service => service.getCharacteristic(UUID_TX_CHAR_CHARACTERISTIC) ).then(characteristic => { tx = characteristic tx.startNotifications() tx.addEventListener('characteristicvaluechanged', e => { const text = new TextDecoder().decode(e.target.value) document.getElementById('recieve_text').value = text + document.getElementById('recieve_text').value }) update(true) }).catch(function(err) { alert(err) }) } else { gatt.disconnect() update(false) } }) </script>
※uart1.html から変更された部分は次のとおりです。
uart1から増えた処理は、gatt を取得したあとに、
server.getPrimaryService(UUID_UART_SERVICE) で micro:bit の UART 機能にアクセスし、
service.getCharacteristic(UUID_TX_CHAR_CHARACTERISTIC) で micro:bit の送信機能のハンドルを取得しています。
そして、tx.addEventListener で micro:bit が送信したデータを受け取る処理を登録し、
tx.startNotifications で受信を開始しています。
microbit側のプログラム変更
「Bluetooth UART 文字列を書き出す」を追加します。
書き出す内容を、 Button A pressed. とします。
左下「ダウンロードボタン」を押して、micro:bit に変更したプログラムを書き込みます。
micro:bit → ブラウザ 文字送信動作確認
ブラウザで uart2.html を開きます。
接続ボタンを押します。
micro:bit の A ボタンを押します。
ブラウザの受信内容に、 Button A pressed. と表示されれば成功です。
ブラウザから micro:bit へ文字を送信する
microbit側のプログラム変更
「Bluetooth データを受信したとき」のブロックを追加します。
表示内容を、「Bluetooth UART つぎのいずれかの文字の手前まで読み取る」にします。
ブラウザ側のプログラム変更
次の内容で、uart3.html を作成します。
<!DOCTYPE html> <meta charset="UTF-8"> <title>Micro:bit UART 通信</title> <style type="text/css"> * { font-size: 3vmin; padding: 3vmin; } body { text-align: center; } textarea { vertical-align: middle; } </style> <h1>Micro:bit UART 通信</h1> <form> <div> <button type="button" id="connect">接続</button> 受信内容<textarea id="recieve_text"></textarea> </div> <div> 送信内容 <input type="text" id="send_text" value="test"> <button type="button" id="send" disabled>送信</button> </div> </form> <script> const UUID_UART_SERVICE = '6e400001-b5a3-f393-e0a9-e50e24dcca9e' const UUID_TX_CHAR_CHARACTERISTIC = '6e400002-b5a3-f393-e0a9-e50e24dcca9e' const UUID_RX_CHAR_CHARACTERISTIC = '6e400003-b5a3-f393-e0a9-e50e24dcca9e' let gatt = null let tx = null let rx = null const update = connected => { document.getElementById('connect').textContent = connected ? '切断' : '接続' document.getElementById('send').disabled = !connected } const send = text => rx.writeValue(new TextEncoder().encode(text + '\n')) document.getElementById('connect').addEventListener('click', e => { if(!(navigator.bluetooth && navigator.bluetooth.requestDevice)) { alert('WebBluetooth に未対応のブラウザです。') return } if (document.getElementById('connect').textContent == '接続') { navigator.bluetooth.requestDevice({ filters: [ { services: [UUID_UART_SERVICE] }, { namePrefix: 'BBC micro:bit' } ] }).then(device => { gatt = device.gatt return gatt.connect() }).then(server => server.getPrimaryService(UUID_UART_SERVICE) ).then(service => Promise.all([ service.getCharacteristic(UUID_TX_CHAR_CHARACTERISTIC), service.getCharacteristic(UUID_RX_CHAR_CHARACTERISTIC)]) ).then(characteristics => { tx = characteristics[0] tx.startNotifications() tx.addEventListener('characteristicvaluechanged', e => { const text = new TextDecoder().decode(e.target.value) document.getElementById('recieve_text').value = text + document.getElementById('recieve_text').value }) rx = characteristics[1] update(true) }).catch(function(err) { alert(err) }) } else { gatt.disconnect() update(false) } }) document.getElementById('send').addEventListener('click', e => { send(document.getElementById('send_text').value) }) </script>
service.getCharacteristic(UUID_RX_CHAR_CHARACTERISTIC) で micro:bit の受信機能のハンドルを取得し、
rx.writeValue でブラウザから文字列を送信しています。
ブラウザ → micro:bit 文字送信動作確認
接続ボタンを押して接続します。
※接続が成功すれば、送信ボタンが押せるようになります。
送信ボタンを押します。成功すると、micro:bit に test という文字がスクロールで表示されます。
以上で、ブラウザと micro:bit 間の文字列送信ができるようになりました。
これをベースに、ブラウザからコマンドを送って、micro:bit にそのコマンドを判別していろいろな動作をさせたり、
micro:bit からセンサーデータをブラウザに送ってブラウザ側でグラフにしたりすることができそうです。
おまけ
計測開始を押すと、micro:bitの光センサーの値が、1秒毎にブラウザに送られてくるプログラムを作ったので共有します。
ブラウザ側
https://programpark.jp/microbit_uart.htmlmicro:bit側
https://makecode.microbit.org/_gJP7Ab3K7hH9
おわりに
日本語は送受信できませんでした。
1度の送信は20文字までのようです。
- 投稿日:2020-01-19T19:01:06+09:00
javascript R
JavaScript関数型プログラミング 複雑性を抑える発想と実践法を学ぶ impress top gearシリーズ Kindle版
https://www.amazon.co.jp/gp/product/B072JVPFL4/ref=ppx_yo_dt_b_d_asin_title_o01?ie=UTF8&psc=1
https://www.webprofessional.jp/functional-programming-with-ramda/> var R = require('ramda') > function isString (test) { ... return R.is(String, test); ... } undefined > > var result = isString('foo'); //=> true undefined > result true最近関数言語に興味がある
- 投稿日:2020-01-19T18:36:20+09:00
ReactでのJest + Enzyme導入
はじめに
本記事は、Udemyの2019 Update! React Testing with Jest and Enzymeという講座を聴講した内容をまとめたものです。自身のメモと、同じ初心者の方のJest導入のつかみになればと思いまとめました。
著者もプログラミング自体始めて2ヶ月程度の初心者のため、間違いや不適切な表現などがありましたらぜひぜひコメント欄にてお知らせください。
Enzyme導入
Enzyme概要について、以下、Enzyme公式docより引用。
Enzyme is a JavaScript Testing utility for React that makes it easier to test your React Components' output. You can also manipulate, traverse, and in some ways simulate runtime given the output.
Enzyme's API is meant to be intuitive and flexible by mimicking jQuery's API for DOM manipulation and traversal.Enzymeをセットアップ
CRA (create-react-app)にはEnzymeがないので別途インストールが必要。
必要なパッケージをインストール。
npm install —save-dev enzyme jest-enzyme enzyme-adapter-react-16 ※enzyme-adapter-react-[version]とする
パッケージをインポート
インストールしたパッケージをインポートし、Enzymeインスタンスのconfigureメソッドを用いて設定を行う。
テストを実行するファイルのファイル名は、
[テスト対象コンポーネント名].test.js
とする。
import ReactDOM from 'react-dom'
は不要なので削除する。App.test.jsimport Enzyme, { shallow } from 'enzyme'; import EnzymeAdapter from 'enzyme-adapter-react-[version]'; Enzyme.configure( { adapter: new EnzymeAdapter() });実際にテストする
テストするコンポーネント
App.jsimport React, { Component } from 'react'; import logo from './logo.svg'; import './App.css'; class App extends Component { render() { return ( <div className="App"> <h1>Hello World</h1> </div> ); } } export default App;テストを実行するファイル
App.test.jstest('renders without crashing', () => { const wrapper = shallow(<App />); expect(wrapper).toBeTruthy(); });shallow()
引数に渡したコンポーネントのみテストを行う。コンポーネント内の子コンポーネントはプレースホルダーとして扱われて実際にはレンダーされないため、子コンポーネントを干渉させずに純粋に単一コンポーネントをテストできる。shallowは「浅い」の意。
wrapper
Enzymeに標準搭載されているAPIで、レンダーされたコンポーネントを格納することで、多くのメソッドを使うことができる。
例えば、ShallowWrapperインスタンスのメソッドの一つであるdebug()は、レンダーしたコンポーネントをHTMLライクなStringとして返す。上記の例のようにAppコンポーネントを格納した状態でconsole.log(wrapper.debug());
とすると、以下のような結果がString(文字列)として返ってくる。<div className="App"> <h1> Hello World </h1> </div>各メソッドについて、詳しくは公式docのShallow Rendering APIを参照。
expect()
Jestの標準搭載メソッドで、テストしたいコンポーネントの様々な値をテストすることができる。例えば、上記例の
toBeTruthy()
ではコンポーネントの値には興味がなく、trueを返すかどうかをテストする。JavaScriptでは、false
,0
,''
,null
,undefined
,NaN
がfalse値として扱われるので、それ以外であればテストが通ることになる。こういったメソッドを使い分けながら様々な値をテストしていく流れ。
各メソッドの詳細は公式docのExpectを参照。さいごに
まだUdemyの動画を見終わっていないので、全て見たら改めて追加情報をまとめようと思います。
- 投稿日:2020-01-19T18:25:28+09:00
reactアプリをnginxを通してhttpsで公開する
確認環境
OS: Amazon Linux release 2 (Karoo)
nginxのインストール
Amazon Linux 2 では標準で Nginx の YUM 向けパッケージが提供されていないため、extraリポジトリからインストールします。
$ sudo amazon-linux-extras install nginx1.12バージョンの確認
$ nginx -v nginx version: nginx/1.12.2nginxの起動
$ sudo service nginx startnginxへのSSL設定
/etc/nginx/conf.d
に、以下のファイルを配置します。
conf
ファイル(nginx設定ファイル)crt
ファイル(サーバー証明書ファイル)csr
ファイル(公開鍵ファイル)key
ファイル(秘密鍵ファイル)オレオレ証明書の作成
$ cd ~ $ openssl genrsa 2048 > server.key $ openssl req -new -key server.key > server.csr $ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt $ sudo mv server.* /etc/nginx/conf.d/ $ cd /etc/nginx/conf.d $ sudo chown root:root server.*※
server.scr
作成時の答えは全てenter
で回答しました。設定ファイルの追加
設定ファイルを追加します。ここでは、
my_ssl_app
という名前にしました。
ちなみに、/etc/nginx/conf.d/*.conf
は、/etc/nginx/nginx.conf
から呼び出されます。ワイルドカード(*.conf
)で呼び出されるため、名前は何でも構いません。$ cd /etc/nginx/conf.d $ touch my_ssl_app.conf
my_ssl_app.conf
を以下のように編集します。server { listen 443 ssl; server_name localhost; ssl_certificate /etc/nginx/conf.d/server.crt; ssl_certificate_key /etc/nginx/conf.d/server.key; location / { proxy_pass http://127.0.0.1:3000; } }nginxの再起動
sudo nginx -s reload?ブラウザで確認
ブラウザからhttps接続して確認すると、502 Bad Gateway のエラーページが表示されます。これは、reactアプリが起動していないためです。
reactアプリの作成
nodejsのインストール(インストール済なら不要)
$ curl --silent --location https://rpm.nodesource.com/setup_10.x | sudo bash - $ sudo yum -y install nodejsreactアプリの作成
$ npx create-react-app my_appアプリの起動
$ cd my_app $ npm start※最新(2020/1/7時点)のreact-scriptでssl接続時に問題が発生しているようです。react-scriptが3.3.0だった場合、リンク先の対応(react-scriptの3.2.0へのバージョンダウン)を行ってください。
?ブラウザで確認
- 投稿日:2020-01-19T18:04:29+09:00
【React】useReducer をもっと自由に活用しよう
useReducer をもっと自由に
皆さん、
useReducer
は活用していますか?
useState
で十分と思っている場合でもuseReducer
に置き換えることで、コードがシンプルかつ、わかりやすくなります。そのためには、Reduxの呪縛を解き払ってください。
useReducer
に Action type は必要ないですし、Flux Standard Action も必要ありません。また、今回はdispatch
もdispatch
らしい使い方はしていません。
つまり、Redux と同じ使い方をする必要はありません。?♀️別にActionType必須ではないconst [state, dispatch] = useReducer((state, action) => { switch(action.type) { case 'FOO_ACTION': return { ...state, foo: action.foo } case 'BAR_ACTION': ...useReducer 活用例
では、
useReducer
を使用すると、どのように変わるのでしょうか。簡単なサンプルコードをもとに紹介します。例)テキストフィールドconst { value, onChange } = useInput() return ( <input type="text" value={value} onChange={onChange} /> )上記はなんの変哲もないテキストフィールドです。
これに合うカスタム Hooks (useInput
) を、useState
とuseReducer
でそれぞれ作成します。useState の場合
useStateを使用した場合export const useInput = () => { const [value, setValue] = useState('') const onChange = useCallback((event) => { setValue(event.currentTarget.value) }, []) return { value, onChange } }
useState
を使用した場合、Hooks は2つ使用します。今回の用途ではuseCallback
はオーバーキル感がありますが、他コンポーネントや他 Hooks で使用する可能性がありますので、メモ化しておくのが良いでしょう。
いずれにしても、onChange
関数を作成するにはsetValue
をラップします。useReducer の場合
useReducerを使用した場合const inputAction = (state, event) => event.currentTarget.value export const useInput = () => { const [value, onChange] = useReducer(inputAction, '') return { value, onChange } }
useState
を使用した場合よりも、シンプルになりました。
useReducer
で記述するメリットは下記の2つです。
- 状態とそれを更新する関数が、ワンセットになる
- reducer 部分は純粋関数であり、Hooks やコンポーネントの外に出せる
1つ目について、
setValue
のような中間の Setter が生まれませんし、用途に合わせる関数(onChange
)を別途作る必要もありません。
また、2つ目の関数外部化は、テストがしやすくなるだけでなく、無駄なオブジェクトを生成しないというパフォーマンス面のメリットもあります。まとめ
useReducer
を使用する場合、Redux 等の使い方に縛られる必要はありません。
単純な state でもuseReducer
に置き換えることで、コードがシンプルかつ、わかりやすくなり、テスタビリティがあがってパフォーマンスも上がります!
- 投稿日:2020-01-19T18:03:00+09:00
Javascriptのspliceで削除すな
概要
Backendで動くJavascriptのコードを引き継いだ際に、ちょっとやばそうなミスをしていたので共有します。
環境
OS:Windows 10
言語:Javascriptsplice関数?
Javascriptの配列標準関数。
大体の使い方は以下を参照。
spliceは切り取るだけのメソッドではなかった問題箇所
問題の箇所は以下。
特定の形のエラーデータをspliceで削除しようとしていた。// ErrorData削除処理 for(var i=0; i<array.length; i++){ if(checkErrorData(array[i]===true)){ array.splice(i, 1); // i番目の要素を削除 } }一見問題なさげに見えるけど大問題。
splice関数は配列のインデックスをめちゃくちゃにするので一連のループ処理で何度も実行するとえらいこっちゃになる。ちょっと掘り下げ
Errorデータを削除するため、i=3時に削除が実行されます。
この時、削除された段階で配列が作り替えられています。forループのカウンタiはそのままインクリメントされるため、元々index=4だった要素に対してErrorチェックが行われません。
問題まとめ
Errorデータの次のデータにチェックが行われない構造になっていますね。
なので以下のデータで削除漏れが起こります。
対策
- splice関数を使ったお姉さんに文句をいいましょう
- 配列から要素を削除する際はfilter関数を使いましょう
- filter関数は以下を参考
【JavaScript入門】filterで配列のデータを抽出する方法
削除というよりはErrorデータ抜きの新しい配列を代入する感じです。
速度的にも圧倒的にこっちの方が早いですね。
以下に書き換えました。// ErrorData削除処理 array = array.filter( function (item) { return checkErrorData(item) === false; })callback関数でitem(配列の各要素)をチェックして、正常なデータの時だけtrue値を返す感じです。
以上、駄文でした。
- 投稿日:2020-01-19T17:32:02+09:00
C/C++に組み込める軽量JavaScriptエンジン “QuickJS” を試す
はじめに
QuickJS は C/C++ に組み込める軽量な JavaScript エンジンである。ライセンスは MIT license。JavaScript を組み込みたいけれど V8 はオーバースペックすぎる、という時に有用と思われる。
デザインがシンプルすぎてかえって信頼感のある公式ページはこちら。
QuickJS is a small and embeddable Javascript engine. It supports the ES2020 specification including modules, asynchronous generators, proxies and BigInt.
(訳:QuickJS は小さい・組み込み可能な JavaScript エンジンである。モジュール・非同期ジェネレータ・プロキシ・BigInt を含めた ES2020 仕様に対応している)ちなみに QuickJS 作者の Fabrice Bellard 氏は qemu や ffmpeg の作者でもある。行動力の化身…(画像略)
また GitHub 上に非公式のミラーがある。これは “Unofficial git mirror” として公式ページからリンクされている。ただし記事執筆現在、最新のリリース (2020-01-05) が反映されておらず 2019-10-27 版のままになっている。
この記事では C/C++ にどうやって QuickJS を組み込んでいくかを概説する。半ば私の備忘録のようなものであり体系的・網羅的ではないのであらかじめご理解のほどを……。
動作確認環境
- Linux
- 最新の Arch Linux (kernel: v5.4.11)
- gcc v9.2.0
- clang v9.0.1 (ついで)
- macOS (ついで)
- Catalina (v10.15.2)
- Apple clang v11.0.0 (clang-1100.0.33.17)
- QuickJS のバージョンは 2020-01-05
- あとインストール時に curl とか GNU tar とか GNU make とかを適宜使っている
インストール
tarball を展開して
make install
するいつもの流れでインストールできる。デフォルトでは/usr/local
下にインストールされるがprefix
指定で変更可能。以下~/.local
にインストールする前提で書くので適宜読み替えること。# ソースをダウンロード curl -LO https://bellard.org/quickjs/quickjs-2020-01-05.tar.xz # tarball を展開 tar axvf quickjs-2020-01-05.tar.xz # ビルドして ~/.local 下にインストールする例 # -j (--jobs) は並列実行数なので適宜調整 make -C quickjs-2020-01-05 -j 2 prefix="${HOME}/.local" install
qjs
コマンドを使いたい場合はPATH
環境変数を適当に通す。PATH="${HOME}/.local/bin:${PATH}" export PATHArch Linux 使いは AUR 、macOS 使いは Homebrew 経由でインストールすることもできる。
qjs
/qjsc
コマンドを使うREPL を起動する
qjs
コマンドを無引数で呼ぶと REPL が起動する。$ qjs QuickJS - Type "\h" for help qjs > \h \h this help \x hexadecimal number display \d *decimal number display \t toggle timing display \clear clear the terminal \q exit qjs > 3**2 + 4**2 25 qjs > 2n ** 256n 115792089237316195423570985008687907853269984665640564039457584007913129639936n qjs > const name = "world" undefined qjs > `Hello, ${name}` "Hello, world" qjs > /^(a)(b*)(c+)(d?)$/.exec("abbbcc") [ "abbbcc", "a", "bbb", "cc", "" ] qjs >JS ファイルを実行する
qjs
コマンドにファイル名を与えるとそのファイルを実行する。import
/export
もできる。greeter.jsexport function greet(name) { console.log(`Hello, ${name}!`); }index.jsimport { greet } from "./greeter.js"; greet("Alice");$ qjs index.js Hello, Alice!JS ファイルをコンパイルする
qjsc
コマンドを使うと JavaScript を実行可能ファイルにできる。$ qjsc index.js $ strip a.out $ file a.out a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=315625503ecf284b44cab3b6f1d3dea6df4dedc7, for GNU/Linux 3.2.0, stripped $ stat -c '%s' a.out | numfmt --to=iec-i 832Ki $ ./a.out Hello, Alice!QuickJS を C/C++ プログラムに埋め込む
ここからが本題。
C API についての公式ドキュメントは以下の数十行のセクションしかない。
まあヘッダ (
quickjs.h
) からだいたい挙動は察せられるしだいたいその通りに動く。あとは REPL のソース (qjs.c
) やらライブラリ本体の実装 (quickjs.c
) やらを読めばそれなりに利用方法はわかってくる。なお、今回例示するソースコードの全体は以下のリポジトリに置いている。
C 側から JS の関数を呼ぶ
手始めに JS で定義した
foo
関数を C API を使って呼んでみる。コードは以下の通り(ちなみにこれは Lua (programming language) - Wikipedia (en) の C API のコード例に対応している)。simple.c#include <stddef.h> #include <stdio.h> #include <string.h> #include <quickjs.h> int main(void) { JSRuntime *rt = JS_NewRuntime(); JSContext *ctx = JS_NewContext(rt); char *const fooCode = "function foo(x, y) { return x + y; }"; if (JS_IsException(JS_Eval(ctx, fooCode, strlen(fooCode), "<input>", JS_EVAL_FLAG_STRICT))) { JS_FreeContext(ctx); JS_FreeRuntime(rt); return -1; } JSValue global = JS_GetGlobalObject(ctx); JSValue foo = JS_GetPropertyStr(ctx, global, "foo"); JSValue argv[] = { JS_NewInt32(ctx, 5), JS_NewInt32(ctx, 3) }; JSValue jsResult = JS_Call(ctx, foo, global, sizeof(argv) / sizeof(JSValue), argv); int32_t result; JS_ToInt32(ctx, &result, jsResult); printf("Result: %d\n", result); JSValue used[] = { jsResult, argv[1], argv[0], foo, global }; for (int i = 0; i < sizeof(used) / sizeof(JSValue); ++i) { JS_FreeValue(ctx, used[i]); } JS_FreeContext(ctx); JS_FreeRuntime(rt); return 0; }上記のソースを以下のコマンドでビルドする。
# コンパイル(-Os でサイズ重視で最適化) gcc -c -Os -Wall -I"${HOME}/.local/include" simple.c # リンク(-Wl,-s で strip) gcc -Wl,-s -L"${HOME}/.local/lib/quickjs" simple.o -l quickjs -l m -o simple実行するとめでたく
5 + 3
の結果が表示される。$ ./simple Result: 8このコードはだいたい以下のような流れで処理を行っている。
JS_NewRuntime
でランタイムの生成
- ランタイム ≈ ブラウザで言うところの Worker 単位と考えてよさそう
- ランタイム内部はシングルスレッドでしか動作しない
- ランタイム間で値を直接共有することはできない
JS_NewContext
でコンテクストの生成
- それぞれのコンテクストが別のグローバルオブジェクトとシステムオブジェクトを持つ
- コンテクスト間では値を直接共有できる
JS_Eval
でコードの実行
JSValue JS_Eval(JSContext *ctx, const char *input, size_t input_len, const char *filename, int eval_flags);
というシグネチャJS_GetGlobalObject
でグローバルオブジェクトの取得JS_GetPropertyStr
でプロパティの取得JS_NewInt32
で整数値を JS 側で使えるようラップJS_Call
で JS の関数を呼び出し
JSValue JS_Call(JSContext *ctx, JSValueConst func_obj, JSValueConst this_obj, int argc, JSValueConst *argv);
というシグネチャJS_ToInt32
で JS の値を C の int に変換JS_FreeValue
で JS のオブジェクトの解放(参照カウンタを減らす)
- GC は参照カウント + Mark & Sweep
- C 側で値を使い終わったことを JS 側に伝える必要がある
- きちんと解放しないと
JS_FreeRuntime: Assertion `list_empty(&rt->gc_obj_list)' failed.
のような怒られが発生するJS_NewInt32
で生成したものは参照型でなく値型なので実はJS_FreeValue
を呼ばなくともよいが、一貫性のため呼んでおくに越したことはない
- 同様に
true
,undefined
,null
のような特別な値をJS_FreeValue
しても害はない- ちなみに参照カウンタを増やしたい場合は
JS_DupValue
を呼ぶJS_FreeValue
しなきゃいけないやつ/してはいけないやつを区別する法則はなんとなく見えてくるが、完全には理解していないので雰囲気でやっている(指摘ください)
Get
したやつやEval
の戻り値はFree
が必要、New
したやつは直後に捨てるなら必要という認識JS_FreeContext
でコンテクストの解放JS_FreeRuntime
でランタイムの解放JS 側から C の関数を呼ぶ
コマンドライン引数として与えられた JavaScript を実行して結果を標準出力に表示するアプリケーションを作る例。動作イメージは以下の通り。
$ ./jseval '3**2 + 4**2' 25 $ ./jseval foo ReferenceError: foo is not defined $ ./jseval 'undefined' $ ./jseval '[3, 4, 5].map(x => x ** 10).forEach(x => console.log(x))' 59049 1048576 9765625コードは以下の通り。
console.log
/console.error
を C で実装し JS から使えるようにしている。jseval.c#include <stdio.h> #include <string.h> #include <quickjs.h> JSValue jsFprint(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv, FILE *f) { for (int i = 0; i < argc; ++i) { if (i != 0) { fputc(' ', f); } const char *str = JS_ToCString(ctx, argv[i]); if (!str) { return JS_EXCEPTION; } fputs(str, f); JS_FreeCString(ctx, str); } fputc('\n', f); return JS_UNDEFINED; } JSValue jsPrint(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) { return jsFprint(ctx, jsThis, argc, argv, stdout); } JSValue jsPrintErr(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) { return jsFprint(ctx, jsThis, argc, argv, stderr); } void initContext(JSContext *ctx) { JSValue global = JS_GetGlobalObject(ctx); // globalThis に console を追加 JSValue console = JS_NewObject(ctx); JS_SetPropertyStr(ctx, global, "console", console); // console.log を設定 JS_SetPropertyStr(ctx, console, "log", JS_NewCFunction(ctx, jsPrint, "log", 1)); // console.error を設定 JS_SetPropertyStr(ctx, console, "error", JS_NewCFunction(ctx, jsPrintErr, "error", 1)); JS_FreeValue(ctx, global); } int main(int argc, char const *argv[]) { int exitCode = 0; JSRuntime *rt = JS_NewRuntime(); JSContext *ctx = JS_NewContext(rt); initContext(ctx); for (int i = 1; i < argc; ++i) { JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_FLAG_STRICT); if (JS_IsException(ret)) { JSValue e = JS_GetException(ctx); jsPrintErr(ctx, JS_NULL, 1, &e); JS_FreeValue(ctx, e); exitCode = 1; break; } else if (JS_IsUndefined(ret)) { // nop } else { jsPrint(ctx, JS_NULL, 1, &ret); } JS_FreeValue(ctx, ret); } JS_FreeContext(ctx); JS_FreeRuntime(rt); return exitCode; }
JS_SetPropertyStr
でプロパティの設定JS_NewCFunction
で C の関数を JS の関数として扱えるようにする
JSValue JS_NewCFunction(JSContext *ctx, JSCFunction *func, const char *name, int length)
というシグネチャ
JSCFunction
の定義はtypedef JSValue JSCFunction(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);
this
と引数(個数と配列先頭のポインタ)を受け取って値を返す関数JSValueConst
はJS_FreeValue
しなくてよいJS_IsException(JS_Evalの戻り値)
が truthy ならJS_GetException
でコンテクスト内で発生した例外のオブジェクトを取得して表示C/C++ のデータを JS 側に管理させる
ここから突如例示コードが C でなく C++ になる。色々面倒になったので……。
例えば C++の
std::mt19937
(擬似乱数生成器)を JavaScript 側で以下のように使いたい、という例を考える。const mt = new Mt19937(); for (let i = 0; i < 10; ++i) { console.log(mt.generate()); // 乱数 (BigInt) を出力する }出力3499211612 581869302 3890346734 3586334585 545404204 4161255391 3922919429 949333985 2715962298 1323567403これは以下のように書ける。
mt19937.cc// Mt19937 class の一意なID(後段で初期化) // 簡単のためグローバルに定義するが、複数 Runtime を同時に動かした時に破綻するので適宜やっていく必要がある static JSClassID jsMt19937ClassID; // Mt19937.prototype.generate JSValue jsMt19937Generate(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) { std::mt19937 *p = static_cast<std::mt19937 *>(JS_GetOpaque(jsThis, jsMt19937ClassID)); return JS_NewBigUint64(ctx, (*p)()); } // Mt19937.prototype const JSCFunctionListEntry jsMt19937ProtoFuncs[] = { JS_CFUNC_DEF("generate", 1, jsMt19937Generate), }; // Mt19937 の constructor JSValue jsMt19937New(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) { // インスタンスを生成 JSValue obj = JS_NewObjectClass(ctx, jsMt19937ClassID); bool fail = false; if (argc == 0) { JS_SetOpaque(obj, new std::mt19937()); } else if (argc == 1) { // ... (シード値を設定したい場合。本質的でないので省略) } else { fail = true; } if (fail) { JS_FreeValue(ctx, obj); // 忘れがち return JS_EXCEPTION; } return obj; } // Mt19937 object が GC に回収された際に呼ばれる void jsMt19937Finalizer(JSRuntime *rt, JSValue val) { std::mt19937 *p = static_cast<std::mt19937 *>(JS_GetOpaque(val, jsMt19937ClassID)); delete p; } // Mt19937 class の定義 // JS 側に表出していないオブジェクト間の依存がある場合 .gc_mark もアレコレする必要があるっぽい JSClassDef jsMt19937Class = { "Mt19937", .finalizer = jsMt19937Finalizer, }; void initContext(JSContext *ctx) { // ... // Mt19937 class の ID を初期化 JS_NewClassID(&jsMt19937ClassID); // ランタイムにクラスを登録 JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class); // prototype 設定 JSValue mt19937Proto = JS_NewObject(ctx); JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>); JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto); // globalThis に Mt19937 を追加 JS_SetPropertyStr(ctx, global, "Mt19937", JS_NewCFunction2(ctx, jsMt19937New, "Mt19937", 1, JS_CFUNC_constructor, 0)); // ... }肝は
JS_NewClass
/JS_NewObjectClass
とJS_GetOpaque
/JS_SetOpaque
。このようにしてstd::mt19937 *
の生存期間の管理を GC に任せることができる。モジュールを利用する
モジュールの
import
/export
を行いたい場合、JS_Eval
時にJS_EVAL_TYPE_MODULE
フラグが必要となる。このフラグが有効な場合、戻り値ret
はJS_UNDEFINED
かJS_EXCEPTION
のいずれかになる。JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT);
qjs
コマンドのようにファイルシステムから JS ファイルをモジュールとしてロードしたい場合、quickjs-libc.c
のjs_module_loader
のような関数をJS_SetModuleLoaderFunc
で登録する必要がある。C/C++ 側でモジュールを定義する
前々節の
Mt19937
について、グローバルプロパティとして与えるのではなくrand
モジュールからimport
するという形にしたい、という場合を考える。import { Mt19937 } from "rand";これは以下のように書ける。
rand.cc// rand module 内の関数一覧 static const JSCFunctionListEntry randFuncs[] = { JS_CFUNC_SPECIAL_DEF("Mt19937", 1, constructor, jsMt19937New), // new Mt19937() できるようにする // JS_CFUNC_DEF("Mt19937", 1, jsMt19937New), // Mt19937() としたいならこっち }; // rand module の初期化(JS の import 時に呼ばれるやつ) int initRand(JSContext *ctx, JSModuleDef *m) { JS_NewClassID(&jsMt19937ClassID); JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class); // prototype 設定 JSValue mt19937Proto = JS_NewObject(ctx); JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>); JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto); // 最後の引数は sizeof(randFuncs) / sizeof(JSCFunctionListEntry) の意 return JS_SetModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>); } // rand module の定義 JSModuleDef *initRandModule(JSContext *ctx, const char *moduleName) { JSModuleDef *m = JS_NewCModule(ctx, moduleName, initRand); if (!m) { return nullptr; } JS_AddModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>); return m; } void initContext(JSContext *ctx) { // ... initRandModule(ctx, "rand"); // ... } int main(int argc, char const *argv[]) { // ... // JS_EVAL_TYPE_MODULE でないと import できない JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT); // ... }
JSCFunctionListEntry[]
を作ってJS_NewCModule
してよしなにやるとモジュールをコンテクストに登録できる。モジュールを共有ライブラリにして
qjs
で実行可能にするモジュール初期化関数の名前を
js_init_module
とし、共有ライブラリとしてコンパイルするとqjs
コマンドで実行するスクリプトからimport
できる。この機能はquickjs-libc.c
のjs_module_loader_so
関数で実装されている。利用イメージimport { Mt19937 } from "./librand.so";rand.cc// rand module の定義 extern "C" JSModuleDef *js_init_module(JSContext *ctx, const char *moduleName) { JSModuleDef *m = JS_NewCModule(ctx, moduleName, initRand); if (!m) { return nullptr; } JS_AddModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>); return m; }g++ -c -fPIC -Os -std=c++17 -Wall -I"${HOME}/.local/include/quickjs" rand.cc # macOS では -undefined dynamic_lookup も付ける g++ -shared -Wl,-s -L"${HOME}/.local/lib/quickjs" rand.o -o librand.soおわりに
QuickJS は完全な JavaScript の処理系が数個の C ソースファイルだけで完結しているのですごい(感想)。ただし 1 ファイルにつき数千〜数万行あるけど。中身は割と読みやすく書かれていそうなのでコードリーディングもしてみたい。
C/C++ に組み込める軽量言語としては他に Lua, mruby, Squirrel 等があるが、代わりに JavaScript (QuickJS) を使うというのも選択肢に入ってくる気がする。例えばアプリケーションの設定ファイルやゲームのスクリプトを JS で書けるようにすると eslint + prettier が使えたり tsc や babel で TypeScript / JSX から変換できたりするのでなんか便利で面白いことができるのではなかろうか(適当)。
あとは実用上 C/C++ ⇆ JavaScript のバインディングを楽に行うやつが必要。Lua で言うところの tolua++ とか luabind とかに相当する層(使ったことないけど今は sol が主流?)。個人的には Rust のマクロでいい感じにやってくれるやつがほしい。現在
quick-js
というラッパーがあるがやや機能が不足しているので自分でも実装したりしなかったりしていきたい。
- quick-js - crates.io: Rust Package Registry
- libquickjs-sys - crates.io: Rust Package Registry (FFI bindings)
参考情報
- 2019 Javascript engine 俯瞰 - abcdefGets
- QuickJS インタプリタの内部動作について触れられている
- 投稿日:2020-01-19T17:19:45+09:00
RailsにおけるAjaxの実装(JavaScriptとjQueryのコード比較)
1. はじめに
RailsでAjaxを実装するには、jQueryを使うのが一般的だと思いますが、ブラックボックスが多すぎて、何をやっているのか、いまいちよく分かりません。
そこで、データの受け渡しの流れを意識しつつ、jQueryで記述したAjax処理を、改めてJavaScriptのみを用いて記述し直してみました。
以下、Ajax通信によるPOSTメソッド(投稿)とDELETEメソッド(削除)について、今回作成したjQuery及びJavaScriptのコードと、使用したメソッドなどについて記録しておきます。
なお、turbolinksは無効にしておかないと、おかしな挙動になってしまいますので、お気をつけください。
誤りなどあれば、ご指摘をいただけると幸いです。実行した環境
- Rails 5.2.4.1
- Ruby 2.5.1
- jQuery 1.12.4
- jquery-rails 4.3.5
- (※ turbolinksは無効にしています)
コードの記載にあたっては、次のような簡易アプリを作って、挙動を確認しています。
ビューファイルは、次のように記載しています。index.html.erb<h4>メモアプリ(サンプル)</h4> <div class="form"> <% if user_signed_in? %> <%= form_with(model: @note, class: "note_form", id: "note_input", local: true) do |form| %> <%= form.text_area :body, class: "note_form-text" %> <%= form.submit "メモを登録", class: "note_form-btn" %> <% end %> <% end %> </div> <div class="notes"> <% @notes.each do |note| %> <div class="note" id="note<%= note.id %>"> <span class="note_name"> 投稿者:<%= note.user.name %> </span> <% if user_signed_in? && note.user_id == current_user.id %> <%= link_to "削除", note_path(note.id) ,class: "note_delete", method: :delete %> <% end %> <%= simple_format note.body, class: "note_body"%> </div> <% end %> </div>2. 投稿(POSTメソッド)についてのAjaxのコード
まずは、POSTメソッド(投稿)についてのAjax処理です。
2-1. jQueryでAjaxを記載(POSTメソッド)
まずは、一般的な、jQueryを使用したサンプルコードです。
データの受け渡しの状況を確認するため、FormDataは使用していません。note.js$(function() { // 追加するHTMLデータを生成する関数 function createHTML(note) { let html = `<div class="note" id="note${note.id}"> <span class="note_name">投稿者:${note.user_name}</span> <a class="note_delete" rel="nofollow" data-method="delete" href="/notes/${note.id}">削除</a> <p class="note_body">${note.body}</p> </div>` return html; } // メモ投稿(POSTメソッド)の処理 $("#note_input").on("submit", function(e) { e.preventDefault(); // デフォルトのイベント(HTMLデータ送信など)を無効にする let inputText = $(".note_form-text").val(); // textareaの入力値を取得 let url = $(this).attr("action"); // action属性のurlを抽出 $.ajax({ url: url, // リクエストを送信するURLを指定 type: "POST", // HTTPメソッドを指定(デフォルトはGET) data: { // 送信するデータをハッシュ形式で指定 "note[body]": inputText }, dataType: "json" // レスポンスデータをjson形式と指定する }) .done(function(data) { let html = createHTML(data); // 受信したデータ(data)を元に追加するURLを生成(createHTML関数は冒頭で定義) $(".notes").append(html); // 生成したHTMLをappendメソッドでドキュメントに追加 $(".note_form-text").val(""); // textareaを空にする }) .fail(function() { alert("error!"); // 通信に失敗した場合はアラートを表示 }) .always(function() { $(".note_form-btn").prop("disabled", false); // submitボタンのdisableを解除 $(".note_form-btn").removeAttr("data-disable-with"); // submitボタンのdisableを解除(Rails5.0以降はこちらも必要) }); }); });コントローラーは、次のように記載しています。
notes_controller.rbclass NotesController < ApplicationController def index @note = Note.new @notes = Note.includes(:user) end def create @note = Note.new(note_params) if @note.save respond_to do |format| format.html { redirect_to root_path } format.json { render json: { body: @note.body, user_name: @note.user.name, user_id: @note.user_id, id: @note.id } } end end end def destroy @note = Note.find(params[:id]) if @note.destroy respond_to do |format| format.html { redirect_to root_path } format.json { render json: { id: params[:id] } } end end end private def note_params params.require(:note).permit(:body).merge(user_id: current_user.id) end end2-1-1. jQueryにおけるAjax通信の基本的な記載項目
2-1-1-1.
$.ajax({})
jQueryでAjax通信をする場合に、送信データとして記載する内容は次のとおりです。
項目 内容 url リクエストを送信するURLを指定(formタグのaction属性にあるURLを指定します) type HTTPメソッドを指定(デフォルトはGET) data 送信するデータをハッシュ形式で指定 dataType サーバから返信されるデータの形式を指定 ●参考サイト(本記事の全般で参考にさせていただいています)
jQuery逆引きリファレンス:一般的なAjax通信を実装するには?
JavaScript 日本語リファレンス jQuery $.ajax()2-1-1-2.
.done(function(data){})
.doneの部分には、通信が成功した場合の処理を記載します。
引数のdataには、受信したデータが格納されています。2-1-1-3.
.fail(function(){})
.failの部分には、通信が失敗した場合の処理を記載します。
2-1-1-4.
.always(function(){})
.alwaysの部分には、通信の成功、失敗に関わらず行う処理を記載します。
ここには、「submitボタンが2回目以降押せなくなる」というRailsの仕様を解除するためのコードを記載しています。
sample.js.always(function() { $(".note_form-btn").prop("disabled", false); // submitボタンのdisableを解除 $(".note_form-btn").removeAttr("data-disable-with"); // submitボタンのdisableを解除(Rails5.0以降はこちらも必要) })submitボタンのdisableを解除するためには、上記の1つ目の処理のように
$(セレクタ名).prop("disabled", false);
と記載します。ただし、Railsの新しいバージョン(Rails5.0以降)では、上記のコードを記載しただけでは、submitボタンの無効が解除されません。
その理由は、ボタンの2度押しを回避するために、"data-disable-with"という属性が自動で追加される仕様となっているためです。これについては、上記の2つ目のように、"data-disable-with"属性自体を消去するコードを追加することで(参照記事:[Rails5] submitタグでAjaxを使うと2回目以降に無効になる)submitボタンがロックされなくなります。
●参考サイト
Rails で JavaScript を使用する / 3.4 入力を自動で無効にする2-1-2. セキュリティトークンについて
jQueryでAjaxを記述する場合は、本来、セキュリティトークンは意識しなくとも良いのですが、JavaScriptでコードを記載する場合に必要になりますので先に触れておきます。
Railsにおいて、POSTメソッド等でサーバにリクエストをする場合は、サーバからクライアントあてに発行されるセキュリティトークン(ワンタイムパスワードのようなもの)を送信データと共に送らなければ、リクエストは受け付けられない仕様になっています。
しかし、今回のサンプルコードでは、特にセキュリティトークンの送信を記述していません。
ログを確認しても、パラメータとしては、Parameters: {"note"=>{"body"=>"こんにちは!"}}
というように、1つのみのデータしか送られていません。log/development.logStarted POST "/notes" for ::1 at 2020-01-19 12:30:52 +0900 Processing by NotesController#create as JSON Parameters: {"note"=>{"body"=>"こんにちは!"}}このように、明示的にセキュリティートークンの送信を指定しなくても、リクエストは正常に処理されています。
なぜこのようにリクエストが正常に成立するかと言うと、jQueryで処理する場合は、サーバにリクエストを送信する際のリクエストヘッダに、自動的にセキュリティトークンが付加されているからでした。
実際のリクエストヘッダは、次のようになっています。上から7行目のX-CSRF-Token:
のところがセキュリティトークンに当たります。RequestHeadersPOST /notes HTTP/1.1 Host: localhost:3000 Connection: keep-alive Content-Length: 69 Accept: application/json, text/javascript, */*; q=0.01 Origin: http://localhost:3000 X-CSRF-Token: gHVpLnJtIFjVzl5VsSESArnw7+sNU67AenEoa29eALi3s9EPl+O5VbM8TnE1QgrA1PbS4Avhdg9atdz2rDcJhg== X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 (以下略)リクエストヘッダの確認方法は、「HTTPレスポンスヘッダ・リクエストヘッダ情報をウェブブラウザで表示・確認する方法」でご確認ください。
●参考サイト
Rails で JavaScript を使用する / 6 AjaxのCSRF(Cross-Site Request Forgery)トークン2-1-3. (参考)FormDataを使用した場合のコード
参考のため、FormDataを使用した場合の記載も掲載しておきます。
省略した部分は、先に記載している「FormDataを使用しない場合」と同様です。note.js$(function() { // (省略) $("#note_input").on("submit", function(e) { e.preventDefault(); let formData = new FormData(this); // FormDataを作成 let url = $(this).attr("action"); $.ajax({ url: url, type: "POST", data: formData, //FormDataをそのまま渡せば良い(必要な"note[body]"と"authenticity_token"を含む) dataType: "json", processData: false, //FormDataを使用した場合に必要となる contentType: false //FormDataを使用した場合に必要となる }) .done(function(data) { // (以下省略) }); }); });FormDataを使用した場合に、どのようなデータが送信されているかを確認しておきます。
ログに表示されているパラメータには、3つのデータが含まれています。
2つ目にある"authenticity_token"
が、フォームに含まれているセキュリティトークンです。中身の文字列はこの次に掲載しているリクエストヘッダと全く同じです。log/development.logStarted POST "/notes" for ::1 at 2020-01-19 13:27:54 +0900 Processing by NotesController#create as JSON Parameters: {"utf8"=>"✓", "authenticity_token"=>"FcqVGUaR69OzydUk9hwJRleLmX0z6auwkgd+NSy1RXwiDC04ox9y3tU7xQByfxGEOo2kdjVbc3+yw4qo79xMQg==", "note"=>{"body"=>"こんばんは!"}}リクエストヘッダでも、7行目のところに
X-CSRF-Token:
とあり、セキュリティトークンが付加されています。RequestHeadersPOST /notes HTTP/1.1 Host: localhost:3000 Connection: keep-alive Content-Length: 446 Accept: application/json, text/javascript, */*; q=0.01 Origin: http://localhost:3000 X-CSRF-Token: FcqVGUaR69OzydUk9hwJRleLmX0z6auwkgd+NSy1RXwiDC04ox9y3tU7xQByfxGEOo2kdjVbc3+yw4qo79xMQg== X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryPiYVoZWTHjJdIdbK (以下略)FormDataでは、以上のようにパラメータ及びリクエストヘッダの両方にセキュリティトークンが付加されています。
実際の挙動を確認しましたところ、どちらか一方にセキュリティトークンがあれば、リクエストは成功するようです。2-2. JavaScriptでAjaxを記載(POSTメソッド)
前置きが長すぎましたが、ここからが本題です。
JavaScriptでPOSTメソッドを実装すると次のようなコードとなります。変に長いですが。note.jswindow.addEventListener("load", function() { let token = document.getElementsByName("csrf-token")[0].content; //セキュリティトークンの取得 // 追加するHTMLデータを生成する関数 function createHTML(note) { // 必要となるタグ及びテキストノードを生成 let divElm = document.createElement("div"); let spanElm = document.createElement("span"); let aElm = document.createElement("a"); let pElm = document.createElement("p"); let nameText = document.createTextNode("投稿者:" + note.user_name); let deleteText = document.createTextNode(" 削除"); let bodyText = document.createTextNode(note.body); // 各タグに属性・属性値を付加 divElm.setAttribute("class", "note"); divElm.setAttribute("id", "note" + note.id); spanElm.setAttribute("class", "note_name"); aElm.setAttribute("class", "note_delete"); aElm.setAttribute("rel", "nofollow"); aElm.setAttribute("data-method", "delete"); aElm.setAttribute("href", "/notes/" + note.id); // aElm.addEventListener("click", function(e) { deleteHTMLEvent(e, aElm) }, false); //ここは削除メソッド実装時に使用 pElm.setAttribute("class", "note_body"); // ノードの結合(各子要素にテキストを追加) spanElm.appendChild(nameText); aElm.appendChild(deleteText); pElm.appendChild(bodyText); // ノードの結合(親要素に子要素を追加) divElm.appendChild(spanElm); divElm.appendChild(aElm); divElm.appendChild(pElm); return divElm; }; // メモ投稿(POSTメソッド)の処理 document.getElementById("note_input").addEventListener("submit", function(e) { e.preventDefault(); // デフォルトのイベント(HTMLデータ送信など)を無効にする //送信データの生成 let inputText = document.getElementsByClassName("note_form-text")[0].value; // textareaの入力値を取得 let url = document.getElementById("note_input").getAttribute("action") + ".json"; // 末尾に[.json]を追加して送信データがjson形式であることを指定 let hashData = { // 送信するデータをハッシュ形式で指定 note: {body: inputText} // 入力テキストを送信 // authenticity_token: token // セキュリティトークンの送信(ここから送信することも可能) }; let data = JSON.stringify(hashData); // 送信用のjson形式に変換 // Ajax通信を実行 let xmlHR = new XMLHttpRequest(); // XMLHttpRequestオブジェクトの作成 xmlHR.open("POST", url, true); // open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か) xmlHR.responseType = "json"; // レスポンスデータをjson形式と指定 xmlHR.setRequestHeader("Content-Type", "application/json"); // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール) xmlHR.setRequestHeader("X-CSRF-Token", token); // リクエストヘッダーを追加(セキュリティトークンの追加) xmlHR.send(data); // sendメソッドでサーバに送信 // 受信したデータの処理 xmlHR.onreadystatechange = function() { if (xmlHR.readyState === 4) { // readyStateが4になればデータの読込み完了 if (xmlHR.status === 200) { // statusが200の場合はリクエストが成功 let note = xmlHR.response; // 受信したjsonデータを変数noteに格納 let html = createHTML(note); // 受信データを元にHTMLを作成 document.getElementsByClassName("notes")[0].appendChild(html); // 作成したHTMLをドキュメントに追加 document.getElementsByClassName("note_form-text")[0].value = ""; // テキストエリアを空白に戻す } else { // statusが200以外の場合はリクエストが適切でなかったとしてエラー表示 alert("error"); } document.getElementsByClassName("note_form-btn")[0].disabled = false; // submitボタンのdisableを解除 document.getElementsByClassName("note_form-btn")[0].removeAttribute("data-disable-with"); // submitボタンのdisableを解除(Rails5.0以降はこちらも必要) } }; }, false); });●参考サイト
基本的なことは、全般的に次の記事を参考にさせていただきました。
JavascriptのAjaxについての基本まとめ2-2-1. ノード(Elementオブジェクト)の取得等
最初に、jQueryとJavaScriptの記述の違いとして、基本的なところを記録しておきます。
上記の、JavaScriptのサンプルコードでは、ノード(Elementオブジェクト)の取得等において、次のようなメソッドを使用しています。
対比が分かりやすいように、表の右側には、対応するjQueryの記述を入れました。
項番 内容 JavaScript jQueryt 1 ID名から取得 document.getElementById("note-id") $("#note-id") 2 class名から取得 document.getElementsByClassName("note_form-text")[0] $(".note_form-text") 3 name名から取得 document.getElementsByName("csrf-token")[0] $("*[name=csrf-token]") 4 属性値を取得(getAttributeメソッド) document.getElementById("note_input").getAttribute("action") $("#note_input").attr("action") 5 ページ全体の読込みができたことの確認 window.addEventListener("load", function() {}) $(function() {}) 表中の項番1から3は、要素(Elementオブジェクト)の取得で使用するメソッドです。
class名、name名を指定してElementオブジェクトを取得する場合、jQueryでは最初に見つかったElementオブジェクトを返しますが、JavaScriptでは該当する全ての要素を返すため、要素を指定する番号[0]を付すことが必要です。
一方、id名を指定してElementオブジェクトを取得する場合は、結果は1つのみ(idは原則として1つしか使用されない)ですので、要素の番号指定は不要です。
idによる指定では、getElementByIdというように、[Element]と単数形になっていることからも、取得するノード数が1つであることが分かります。2-2-2. HTMLデータの生成
ページに追加するHTML作成において使用しているメソッドについて、jQueryと比較しつつ簡単にまとめておきます。
例として、投稿者名とコメントを表示するだけの簡単なHTMLを書いてみます。
まず、jQueryでは、次のように、バッククオートでHTML文を囲むことで、簡単にHTMLの生成ができます。sample.jslet html = `<div class="comment"> <p class="comment_name">投稿者:山田</p> こんにちは! </div>`これと同じ内容を、JavaScriptで記述すると、次のようになります。
sample.jslet divElm = document.createElement("div"); // divタグを生成: <div></div> divElm.setAttribute("class", "comment"); // class名を追加: <div class="comment"></div> let pElm = document.createElement("p"); // pタグを生成: <p></p> pElm.setAttribute("class", "omment_name"); // class名を追加: <p class="comment_name"></p> pElm.appendChild(document.createTextNode("投稿者:山田")); // テキストノードを追加: <p class="comment_name">投稿者:山田</p> let commentText = document.createTextNode("こんにちは!"); // テキストノードを生成: "こんにちは!" divElm.appendChild(pElm); // divタグ(divElm)の子要素にpタグ(pElm)を追加 divElm.appendChild(commentText); // divタグ(divElm)の子要素にテキストノード("こんにちは!")を追加 let html = divElm;JavaScriptにおけるHTML生成のためのメソッドは、主に次のとおりです。
これらを組み合わせて、HTMLを生成していくということになります(もっと簡単な方法があれば良いのですが)。
項番 項目 構文 具体例 1 要素(タグ)の生成 document.createElement(タグ名) document.createElement("div") 2 テキストノードの生成 document.createTextNode(テキスト文) document.createTextNode("こんにちは!") 3 属性の追加 要素ノード.setAttribute(属性名, 属性値) divElm.setAttribute("class", "comment") 4 子要素として追加 親ノード.appendChild(子ノード) divElm.appendChild(pElm) ●参考サイト
【JavaScript入門】appendと何が違う?appendChild徹底解説2-2-3. サーバへのリクエスト送信
sample.jslet xmlHR = new XMLHttpRequest(); // XMLHttpRequestオブジェクトの作成 xmlHR.open("POST", url, true); // open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か) xmlHR.responseType = "json"; // レスポンスデータをjson形式と指定 xmlHR.setRequestHeader("Content-Type", "application/json"); // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール) xmlHR.setRequestHeader("X-CSRF-Token", token); // リクエストヘッダーを追加(セキュリティトークンの追加) xmlHR.send(data); // sendメソッドでサーバに送信サーバにリクエストを送信するために使用しているオブジェクト及びメソッドは、次のとおりです。
項番 項目 内容 1 XMLHttpRequestオブジェクト サーバとの通信を行うためのオブジェクト(API)。このオブジェクトにより、ページ全体の更新をすることなくサーバとの送受信を行うことができる。 2 XMLHttpRequest.open()メソッド XMLHttpRequestオブジェクトのメソッド。リクエストの作成に使用する。 3 XMLHttpRequest.responseTypeメソッド XMLHttpRequestオブジェクトのメソッド。サーバから返信されるデータの形式を指定。 4 XMLHttpRequest.setRequestHeaderメソッド XMLHttpRequestオブジェクトのメソッド。リクエストヘッダーを追加する。 5 XMLHttpRequest.send()メソッド XMLHttpRequestオブジェクトのメソッド。引数に指定したデータをサーバに送信する。 2-2-3-1. XMLHttpRequestオブジェクト
まず、1つめのXMLHttpRequestオブジェクトですが、これについては、次の説明が直感的に分かりやすいです。
XMLHttpRequestはブラウザ上でサーバーとHTTP通信を行うためのAPIです。名前にXMLが付いていますがXMLに限ったものではなく,HTTPリクエストを投げてテキスト形式かDOMノードでレスポンスを受け取る機能を持っています。(これでできる! クロスブラウザJavaScript入門)
このXMLHttpRequestオブジェクトを介して、サーバへのリクエストを作成してデータのやり取りを行うことになります。
●参考サイト
MDN Web Docs / XMLHttpRequest2-2-3-2. XMLHttpRequest.open()メソッド
構文:
XMLHttpRequest.open(HTTPメソッド, URL, 非同期通信か同期通信か)
このopen()メソッドは、リクエストを作成する場合に使用します。
第1引数でHTTPメソッド、第2引数でURL、第3引数で非同期通信[true]か同期通信[false]かを指定します。第3引数はデフォルトがtrueなので、一般的には省略されているようです。更に、第4引数(ユーザ名)、第5引数(パスワード)もありますので、詳しくは「MDN Web Docs / XMLHttpRequest.open()」を参照してください。
なお、JSON形式で送信する場合、URLの指定において、末尾に
.JSON
を付すことが必要です。これにより、送信データがjson形式であることが指定されます。2-2-3-3. XMLHttpRequest.responseTypeメソッド
responseTypeメソッドでは、サーバから返信されるデータの形式を指定します。
ここでは、返信されるデータ形式として"json"
を指定していますが、そのほかの形式として、"arraybuffer"
、"blob"
、"document"
、"text"
などの形式の指定ができます(詳細は「MDN Web Docs - XMLHttpRequest.responseType」を参照してください)。2-2-3-4. XMLHttpRequest.setRequestHeaderメソッド
これは、HTTP通信におけるリクエストヘッダを追加するメソッドです(参考「MDN Web Docs / XMLHttpRequest.setRequestHeader()」)。
sample.jsxmlHR.setRequestHeader("Content-Type", "application/json"); // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール) xmlHR.setRequestHeader("X-CSRF-Token", token); // リクエストヘッダーを追加(セキュリティトークンの追加)JSON形式のリクエストを送信する場合には、上記1行目のように、Content-Typeヘッダに"application/json"(又は"text/json")というMIMEタイプの指定が必要となります(引用サイト:JSON リクエストとレスポンス)。
2行目のコードは、セキュリティトークンをリクエストヘッダに追加する処理です。
このセキュリティトークンの詳細は、次の項「2-2-4. セキュリティトークンの送信」に書いています。●参考サイト
XMLHttpRequestでJSONをPOST
ajaxでpostするときに必要なリクエストヘッダ2-2-3-5. XMLHttpRequest.send()メソッド
send()メソッドは、リクエストをサーバに送信するメソッドです。送信するデータを引数に指定することができます(参考「MDN Web Docs / XMLHttpRequest.send()」)。
関係部分を抜粋すると、次のようになっています。sample.jslet hashData = { // 送信するデータをハッシュ形式で指定 note: {body: inputText} // 入力テキストを送信 }; let data = JSON.stringify(hashData); // 送信用のjson形式に変換 xmlHR.send(data); // sendメソッドでサーバにリクエストを送信上記のコードでは、まず、ハッシュオブジェクトとしてデータを作成し、これを、JSON.stringifyメソッドを使用して、JSON文字列に変換しています(参考「MDN Web Docs / JSON.stringify()」)。
これにより、引数に指定したデータをJSON形式として送信することができます。2-2-4. セキュリティトークンの送信
JavaScriptでAjaxを実装する場合には、セキュリティトークン(サーバから発行されるワンタイムパスワードのようなもの)を意識する必要があります。
自動的にセキュリティトークンが送信されるjQueryの場合とは異なり(前出の「2-1-2. セキュリティトークンについて」を参照)、JavaScriptでAjaxを記述する場合には、サーバへのHTTPリクエストにセキュリティトークンを付加する処理が必要となります。
この処理を書かなければ、POSTメソッドやDELETEメソッドによるリクエストを、サーバ側で受け付けることができません。サンプルコードから、セキュリティトークンに関する部分を抜粋すると、次のようになっています。
sample.jslet token = document.getElementsByName("csrf-token")[0].content; //セキュリティトークンの取得 // (中略) let xmlHR = new XMLHttpRequest(); // XMLHttpRequestオブジェクトの作成 // (中略) xmlHR.setRequestHeader("X-CSRF-Token", token); // リクエストヘッダーを追加(セキュリティトークンの追加)上記の最初の行の処理で、ページのHTMLヘッダからセキュリティトークンの情報を抽出し、最後の行で、リクエストヘッダにセキュリティトークンを追加する処理を行っています。
なお、セキュリティトークンは、ページのHTMLを見れば確認できます。
(HTMLのヘッダ部分に表示されているセキュリティトークン)
sample.html<head> <title>DemoApp</title> <meta name="csrf-param" content="authenticity_token"> <meta name="csrf-token" content="RqHWJuRG/kobdPDTVdQEaJAl7hwpmfptmuJbamK/+5JxZ24HAchnR32G4PfRtxyq/SPTFy8rIqK6Jq/3odbyrA=="> <!-- 略 --> </head>ヘッダ部分の3行目にある
<meta name="csrf-token" content="RqHWJuRG/kobdPDTVdQEaJAl7hwpmfptmuJbamK/+5JxZ24HAchnR32G4PfRtxyq/SPTFy8rIqK6Jq/3odbyrA==">
がセキュリティトークンです。
この、content
の属性値を拾ってサーバに送信していると言うことになります。また、Formタグ(下記)の中にも、
name="authenticity_token"
の属性value
に、セキュリティートークンが格納されています。こちらからデータを拾って送信しても同様の結果を得ることができます。sample.html<form id="note_input" class="note_form" action="/notes" accept-charset="UTF-8" method="post"> <input name="utf8" type="hidden" value="✓"> <input type="hidden" name="authenticity_token" value="RqHWJuRG/kobdPDTVdQEaJAl7hwpmfptmuJbamK/+5JxZ24HAchnR32G4PfRtxyq/SPTFy8rIqK6Jq/3odbyrA=="> <textarea class="note_form-text" name="note[body]" id="note_body"></textarea> <input type="submit" name="commit" value="メモを登録" class="note_form-btn" data-disable-with="メモを登録"> </form>詳細は、Rails で JavaScript を使用する / 6 AjaxのCSRF(Cross-Site Request Forgery)トークンを参照してください。
2-2-5. サーバからのレスポンスの受信
2-2-5-1. レスポンス処理の構造
JavaScriptにおいては、次のように記述することで、リクエストの成否に合わせて処理が実行されます。
(JavaScriptにおけるレスポンスの処理)
sample.jsxmlHR.onreadystatechange = function() { if (xmlHR.readyState === 4) { // readyStateが4になればデータの読込み完了 if (xmlHR.status === 200) { // statusが200の場合はリクエストが成功 // (1) リクエストが成功した場合に行う処理 } else { // statusが200以外の場合はリクエストが適切でなかったとしてエラー表示 // (2) リクエストが成功しなかった場合に行う処理 } // (3) リクエストの成功・失敗に関わらず行う処理 } };下記は、jQueryでのコード記載です。JavaScriptにおける(1)から(3)の処理とほぼ同じ形で対応しています(細かい違いはあるかもしれませんが)。
(jQueryにおけるレスポンスの処理)
sample.js.done(function(data) { // (1) リクエストが成功した場合に行う処理 }) .fail(function() { // (2) リクエストが成功しなかった場合に行う処理 }) .always(function() { // (3) リクエストの成功・失敗に関わらず行う処理 });2-2-5-2. レスポンスデータの取得について
JavaScriptのレスポンスデータは、次のように取得することができます。
(JavaScriptにおけるレスポンスデータの取得)sample.jsif (xmlHR.status === 200) { // statusが200の場合はリクエストが成功 let note = xmlHR.response; // XMLHttpRequest.responseメソッドからレスポンスデータを取得できる // (略) }対比として、jQueryにおけるレスポンスデータの取得も以下に記載しておきます。
(jQueryにおけるレスポンスデータの取得)sample.js.done(function(data) { let note = data; // 引数のdataからレスポンスデータを取得できる // (略) })2-2-5-3. readyStateプロパティとstatusプロパティについて
XMLHttpRequest.readyStateプロパティ
これは、XMLHttpRequestオブジェクトのプロパティとなります。
readyStateプロパティは、XMLHttpRequestのインスタンスの状態を0から4の数値で返します。
数値と状態の対比は次のとおりです(引用サイト「MDN Web Docs / XMLHttpRequest.readyState」→こちらを見ていただいた方が正確です)。
戻り値 状態 内容 0 UNSENT XMLHttpRequestのインスタンス作成済み 1 OPENED open()メソッド呼び出し済み 2 HEADERS_RECEIVED send()メソッド呼び出し済み 3 LOADING レスポンスデータの読み込み中 4 DONE 読み込み完了 サンプルコードでは、
if (xmlHR.readyState === 4)
という条件を満たした場合に、レスポンスデータの処理等を行うという構造になっています。XMLHttpRequest.statusプロパティ
このXMLHttpRequest.statusプロパティには、サーバから送信される「HTTPステータスコード」が格納されています(引用サイト「MDN Web Docs / XMLHttpRequest.status」)。
このHTTPステータスコードにより、HTTPリクエストが正常に終了したかどうかが分かります。
下記は今回の記事作成中に見かけたHTTPステータスコードです(ほんの一例です)。
コード 意味 説明 200 OK リクエストが成功した 400 Bad Request 無効なリクエストが送信されたなど 404 Not Found リクエストされたリソースが見つからなかった 422 Unprocessable Entity リクエストは正しいがサーバで処理できない 例えば、セキュリティトークンが送信されていない場合、422のエラーが生じます。
その他、コードの一覧は「MDN Web Docs / HTTP レスポンスステータスコード」などで確認できます。サンプルコードでは、
if (xmlHR.status === 200)
という条件を満たした場合にはリクエスト成功時の処理を記述し、条件を満たさなかった場合にはリクエスト失敗時の処理を記述するという構造になっています。3. 削除(DELETEメソッド)についてのAjaxのコード
次に、投稿内容を削除するDELETEメソッドについてのAjax通信です。
3-1. jQueryでAjaxを記載(DELETEメソッド)
まず、jQueryでの実装例です。
新たに削除(DELETE)メソッドを追加した部分は、後半の20行分となります。note.js$(function() { // 追加するHTMLデータを生成する関数 function createHTML(note) { let html = `<div class="note" id="note${note.id}"> <span class="note_name">投稿者:${note.user_name}</span> <a class="note_delete" rel="nofollow" data-method="delete" href="/notes/${note.id}">削除</a> <p class="note_body">${note.body}</p> </div>` return html; } // メモ投稿(POSTメソッド)の処理 $("#note_input").on("submit", function(e) { e.preventDefault(); // デフォルトのイベント(HTMLデータ送信など)を無効にする let inputText = $(".note_form-text").val(); // textareaの入力値を取得 let url = $(this).attr("action"); // action属性のurlを抽出 $.ajax({ url: url, // リクエストを送信するURLを指定 type: "POST", // HTTPメソッドを指定(デフォルトはGET) data: { // 送信するデータをハッシュ形式で指定 "note[body]": inputText }, dataType: "json" // レスポンスデータをjson形式と指定する }) .done(function(data) { let html = createHTML(data); // 受信したデータ(data)を元に追加するURLを生成(createHTML関数は冒頭で定義) $(".notes").append(html); // 生成したHTMLをappendメソッドでドキュメントに追加 $(".note_form-text").val(""); // textareaを空にする }) .fail(function() { alert("error!"); // 通信に失敗した場合はアラートを表示 }) .always(function() { $(".note_form-btn").prop("disabled", false); // submitボタンのdisableを解除 $(".note_form-btn").removeAttr("data-disable-with"); // submitボタンのdisableを解除(Rails5.0以降はこちらも必要) }); }); // メモ削除(DELETEメソッド)の処理 $(".notes").on("click", ".note_delete", function(e) { e.preventDefault(); // デフォルトのイベント(リンクURLへの遷移処理など)を無効にする e.stopPropagation(); // 現在のイベントのさらなる伝播(DELETEメソッドの実行)を止める let url = $(this).attr("href"); $.ajax({ url: url, type: "POST", // 原則に従って"DELETE"メソッドを使用しない data: { _method: "delete", // ここで"DELETE"メソッドを使用することを指定 }, dataType: "json" }) .done(function(data) { $("#note" + data.id).remove(); // レスポンスデータのIDを元に投稿を削除 }) .fail(function(XMLHttpRequest) { alert(XMLHttpRequest.status); }); }); });基本的な形式は、POSTメソッドと変わりません。
異なる部分を中心に、以下、説明を書いていきます。3-1-1. 動的に追加した要素をクリックする(jQuery)
前提として、jQuery(JavaScript)のファイル読み込み処理について確認しておきます。
まず、jQuery(JavaScript)のコードを記載したファイルの読み込みですが、これは、最初のページ読み込み時にのみ行われます。
次のような、クリックなどのイベント発火による処理も、クリックされるたびにファイルが読み込まれているわけではなく、最初の1回のみ読み込まれて、それをブラウザ内に記憶しておき、都度ブラウザ内の記憶を呼び出して実行しているということになります。sample.js$("#note_input").on("submit", function(e) { // イベント発火時に実行する内容 })このようにイベントに基づき発火するメソッド(関数)は、「イベントリスナー(イベント実行リストのようなもの)」という名目でブラウザ内に登録されています。
以上のことから、ページを読み込んだ後(JavaScriptのファイルの読み込みが終わった後)に、動的に新たに追加された要素は、イベントリスナーとして登録されないことになるため、クリックしても反応しないということになってしまいます。
jQueryでは、この問題を、簡単に解決できるようになっています。
サンプルコード上で、その処理を実現している部分は、次の部分となります。sample.js$(".notes").on("click", ".note_delete", function(e) { // イベント発火時の処理内容を記載 });これを構文として書き表すと次のようになります。
$(親要素のセレクタ).on(イベントの種類, 子要素のセレクタ, 関数等のオブジェクト)
この構文において、クリック(他のイベントも同じ)によるイベント発火は、見かけ上、子要素へのクリックに基づき実行されます。
ところが、実際は、親要素へのクリックにより、イベント発火が行われているという仕組みになっています。そのため、イベント発火の元となる親要素は、最初(ページ読み込み時)に存在している必要がありますが、子要素は、後から追加された動的な要素であっても構わないという仕組みになっています(と理解しています)。
具体的にこのjQueryのメソッドどのような構造なのかについては、下記のサイトを参考にしていただければと思います。
●参考サイト
[jQuery] on() で後から追加した要素にもイベントを定義したい
jQuery write less, do more. / .on()3-1-2. preventDefault()メソッド及びstopPropagation()メソッド
次に、デフォルトの処理等をキャンセルするpreventDefault()メソッド及びstopPropagation()メソッドについてです。
サンプルコードでは、次のように記載しています。sample.js$(".notes").on("click", ".note_delete", function(e) { e.preventDefault(); // デフォルトのイベント(リンクURLへの遷移処理)を無効にする e.stopPropagation(); // 現在のイベントのさらなる伝播(DELETEメソッドの実行)を止める // (略) });これらの記述により、HTMLに基づくデフォルトのイベント処理をキャンセルすることで、無用の処理を発生させず、Ajaxでの処理との重複などを避けることができます。
以下、個別に見てみます。3-1-2-1. Event.preventDefault()メソッド
preventDefault()メソッドは、デフォルトのイベント処理をキャンセルして実行しないようにするメソッドです(参照:MDN Web Docs / Event.preventDefault())。
サンプルコードでは、次のaタグにおけるリンク機能が、preventDefault()メソッドによりキャンセルされています。
sample.html<a class="note_delete" rel="nofollow" data-method="delete" href="/notes/5">削除</a>3-1-2-2. event.stopPropagation()メソッド
stopPropagation()メソッドは、現在のイベントの更なる伝播をキャンセルするメソッドです(参照:MDN Web Docs / event.stopPropagation)。
サンプルコードでは、上記aタグの属性
data-method="delete"
によるdeleteメソッドの実行が、stopPropagation()メソッドによりキャンセルされています。なお、全ての削除処理をAjax通信のみで行うのであれば、
data-method="delete"
の部分を消してしまうことでも同様の結果が得られると思います(stopPropagation()メソッドがなくてもエラーなく処理が実行できることになります)。3-1-3. HTTPにおけるDELETEメソッドについて
3-1-3-1. POSTメソッドによる削除
サンプルコードでは、削除(DELETE)の処理であるにも関わらず、次のように、
type
として"POST"
メソッドを指定しています。sample.js$.ajax({ url: url, type: "POST", // 原則に従って"DELETE"メソッドを使用しない data: { _method: "delete", // ここで"DELETE"メソッドを使用することを指定 }, dataType: "json" })POSTメソッドを使用して、削除機能を実装する理由は、「jQueryの日本語リファレンス / $.ajax()」にある次の説明のとおりで、DELETEメソッドが全てのブラウザでサポートされている保証がないからです。
キー:type
型:String 初期値:'GET'
リクエストのタイプ("POST"または"GET")を指定します。
注意: PUTやDELETEのような、他のHTTPリクエストメソッドも、ここで指定することが可能ですが、 全てのブラウザでサポートされている保証がありません。具体的な、コードの記載については、次の記事などを参考とさせていただきました。
[Laravel / jQuery] 非同期(Ajax)でレコードを削除したい3-1-3-2. DELETEメソッドによる削除
どこまでの環境で動作するかは確認できていませんが、次のようにDELETEメソッドによる記載をしても、Ajaxによる削除を行うことが可能です。
sample.js$.ajax({ url: url, type: "DELETE", // 原則に従って"DELETE"メソッドを使用しない dataType: "json" })3-2. JavaScriptでAjaxを記載(DELETEメソッド)
次に、JavaScriptでの削除(DELETEメソッド)機能の実装例です。
note.jswindow.addEventListener("load", function() { let token = document.getElementsByName("csrf-token")[0].content; //セキュリティトークンの取得 // 追加するHTMLデータを生成する関数 function createHTML(note) { // 必要となるタグ及びテキストノードを生成 let divElm = document.createElement("div"); let spanElm = document.createElement("span"); let aElm = document.createElement("a"); let pElm = document.createElement("p"); let nameText = document.createTextNode("投稿者:" + note.user_name); let deleteText = document.createTextNode(" 削除"); let bodyText = document.createTextNode(note.body); // 各タグに属性・属性値を付加 divElm.setAttribute("class", "note"); divElm.setAttribute("id", "note" + note.id); spanElm.setAttribute("class", "note_name"); aElm.setAttribute("class", "note_delete"); aElm.setAttribute("rel", "nofollow"); aElm.setAttribute("data-method", "delete"); aElm.setAttribute("href", "/notes/" + note.id); aElm.addEventListener("click", function(e) { deleteHTMLEvent(e, aElm) }, false); //ここが大事! pElm.setAttribute("class", "note_body"); // ノードの結合(各子要素にテキストを追加) spanElm.appendChild(nameText); aElm.appendChild(deleteText); pElm.appendChild(bodyText); // ノードの結合(親要素に子要素を追加) divElm.appendChild(spanElm); divElm.appendChild(aElm); divElm.appendChild(pElm); return divElm; }; // メモ投稿(POSTメソッド)の処理 let addHTMLEvent = function(e) { e.preventDefault(); // デフォルトのイベント(HTMLデータ送信など)を無効にする //送信データの生成 let inputText = document.getElementsByClassName("note_form-text")[0].value; // textareaの入力値を取得 let url = document.getElementById("note_input").getAttribute("action") + ".json"; // 末尾に[.json]を追加して送信データがjson形式であることを指定 let hashData = { // 送信するデータをハッシュ形式で指定 note: {body: inputText} // 入力テキストを送信 // authenticity_token: token // セキュリティトークンの送信(ここから送信することも可能) }; let data = JSON.stringify(hashData); // 送信用のjson形式に変換 // Ajax通信を実行 let xmlHR = new XMLHttpRequest(); // XMLHttpRequestオブジェクトの作成 xmlHR.open("POST", url, true); // open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か) xmlHR.responseType = "json"; // レスポンスデータをjson形式と指定 xmlHR.setRequestHeader("Content-Type", "application/json"); // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール) xmlHR.setRequestHeader("X-CSRF-Token", token); // リクエストヘッダーを追加(セキュリティトークンの追加) xmlHR.send(data); // sendメソッドでサーバに送信 // 受信したデータの処理 xmlHR.onreadystatechange = function() { if (xmlHR.readyState === 4) { // readyStateが4になればデータの読込み完了 if (xmlHR.status === 200) { // statusが200の場合はリクエストが成功 let note = xmlHR.response; // 受信したjsonデータを変数noteに格納 let html = createHTML(note); // 受信データを元にHTMLを作成 document.getElementsByClassName("notes")[0].appendChild(html); // 作成したHTMLをドキュメントに追加 document.getElementsByClassName("note_form-text")[0].value = ""; // テキストエリアを空白に戻す } else { // statusが200以外の場合はリクエストが適切でなかったとしてエラー表示 alert("error"); } document.getElementsByClassName("note_form-btn")[0].disabled = false; // submitボタンのdisableを解除 document.getElementsByClassName("note_form-btn")[0].removeAttribute("data-disable-with"); // submitボタンのdisableを解除(Rails5.0以降はこちらも必要) } }; }; document.getElementById("note_input").addEventListener("submit", addHTMLEvent, false); // 削除イベントの処理 let deleteHTMLEvent = function(e, noteDelete) { e.preventDefault(); e.stopPropagation(); let url = noteDelete.getAttribute("href") + ".json"; //末尾に .json を追加することで送信データがjson形式であることを指定する // Ajax通信を実行 let xmlHR = new XMLHttpRequest(); //XMLHttpRequestの作成 xmlHR.open("DELETE", url, true); //open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か) xmlHR.responseType = "json"; //レスポンスデータを json形式と指定する xmlHR.setRequestHeader("Content-Type", "application/json"); // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール) xmlHR.setRequestHeader("X-CSRF-Token", token); // リクエストヘッダーを追加(セキュリティトークンの追加) xmlHR.send(); //サーバに送信 // 受信したデータの処理 xmlHR.onreadystatechange = function() { if (xmlHR.readyState === 4) { if (xmlHR.status === 200) { let note = xmlHR.response; document.getElementById("note" + note.id).remove(); } else { alert(xmlHR.status); } } }; }; let noteDeletes = document.getElementsByClassName("note_delete") Array.prototype.forEach.call(noteDeletes, function(noteDelete) { noteDelete.addEventListener("click", function(e) { deleteHTMLEvent(e, noteDelete) }, false); }); });POSTメソッドのサンプルコードに、末尾の27行を追加した上で、その他に若干の修正を行っています。
3-2-1. AjaxによるDELETEメソッドの送信(JavaScript)
Ajax通信を行うために、XMLHttpRequestオブジェクトを使ってサーバと通信するという点は、基本的にPOSTメソッドの場合と変わりません。
該当部分のコードは次のとおりです。
sample.jslet xmlHR = new XMLHttpRequest(); //XMLHttpRequestの作成 xmlHR.open("DELETE", url, true); //open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か) xmlHR.responseType = "json"; //レスポンスデータを json形式と指定する xmlHR.setRequestHeader("Content-Type", "application/json"); // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール) xmlHR.setRequestHeader("X-CSRF-Token", token); // リクエストヘッダーを追加(セキュリティトークンの追加) xmlHR.send(); //サーバに送信異なると言えば、send()メソッドの送信時に、引数としてデータを指定していないということくらいでしょうか。
削除時のログは、次のようになっています。
log/development.logStarted DELETE "/notes/591.json" for ::1 at 2020-01-19 15:53:48 +0900 Processing by NotesController#destroy as JSON Parameters: {"id"=>"591", "note"=>{}}send()メソッドの引数として
"id"
は特に指定していませんが、パラメータとしてしっかり送られています。
URLの情報に"id"が含まれているので、当然と言えば当然だと思います。なお、このJavaScriptにおいても、POSTメソッドを使用した削除機能(DELETEメソッド)の実装を試みましたが、設定が上手く行かず、現時点では実装できていません。
3-2-2. 複数の要素にイベントリスナーを適用する方法(JavaScript)
POSTメソッドの実装時に、イベントリスナーを適用するノード(要素、タグ)は、formタグの中のsubmitボタンの1箇所だけでしたが、DELETEメソッドは、表示されている全ての投稿にイベントリスナーを設定する必要があります。
投稿の全てに、イベントリスナーを設定するには、次のように、forEachなどを用いて該当する全ての要素にイベントリスナーを登録することで足ります。
この方法は、「【JavaScript】イベントリスナを複数要素にまとめて登録する方法」で解説されている内容を使用させていただきました。sample.jslet noteDeletes = document.getElementsByClassName("note_delete") Array.prototype.forEach.call(noteDeletes, function(noteDelete) { noteDelete.addEventListener("click", function(e) { deleteHTMLEvent(e, noteDelete) }, false); });これは、for文で単純にループさせることでも可能です。
なお、コールバック関数に引数があるため、
function(e) { deleteHTMLEvent(e, noteDelete) },
という特殊な書き方になっています。
これについては、「Javascriptでイベントハンドラのコールバック関数に引数を渡す」の記事等を参考にさせていただきました。詳細はリンク先を参照してください。●参考サイト
配列ライクなオブジェクトをforEachするときのイディオム
JavaScriptでコールバック関数にあらかじめ引数を渡したい!3-2-3. 動的に追加された要素へのイベントリスナーの適用(JavaScript)
動的に追加された要素(本サンプルではメモ投稿を表示するノード)に、後からイベントリスナーを登録するには、多少の工夫が必要となります。
大事なところは、createHTML関数の中に追加した次の1行です(上から21行目)。
sample.jsaElm.addEventListener("click", function(e) { deleteHTMLEvent(e, aElm) }, false); //ここが大事!新規投稿のHTMLが生成がされる度に、上記の1行でaタグに新しいイベントリスナーを追加するようにしています。
ここでコールバック関数として呼び出されているのは
function(e) { deleteHTMLEvent(e, aElm) }
の部分です(コールバック関数に引数を渡す方法については、「Javascriptでイベントハンドラのコールバック関数に引数を渡す」を参照)。呼びだされるコールバック関数は、最初にページをロードする際に読み込む関数と同じもので、具体的には、次の部分となります。
sample.jslet deleteHTMLEvent = function(e, noteDelete) { e.preventDefault(); e.stopPropagation(); let url = noteDelete.getAttribute("href") + ".json"; //末尾に .json を追加することで送信データがjson形式であることを指定する // Ajax通信を実行 let xmlHR = new XMLHttpRequest(); //XMLHttpRequestの作成 xmlHR.open("DELETE", url, true); //open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か) xmlHR.responseType = "json"; //レスポンスデータを json形式と指定する xmlHR.setRequestHeader("Content-Type", "application/json"); // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール) xmlHR.setRequestHeader("X-CSRF-Token", token); // リクエストヘッダーを追加(セキュリティトークンの追加) xmlHR.send(); //サーバに送信 // 受信したデータの処理 xmlHR.onreadystatechange = function() { if (xmlHR.readyState === 4) { if (xmlHR.status === 200) { let note = xmlHR.response; document.getElementById("note" + note.id).remove(); } else { alert(xmlHR.status); } } }; };新規の投稿がページに追加される度に、上記の関数が呼び出されて、イベントリスナーとして追加登録されているということになります。
●参考サイト
[JavaScript] イベント処理を動的に追加する
動的に追加した要素にaddEventListnerを設定する方法4. おわりに
まとめておきたかった内容は、以上のところです。
挙動は確認しているので、動作はするはずですが、もっと良い書き方や、正しい書き方があるのだろうと思います。
お気付きのことがあれば、ご指摘等をいただけると幸いです。
- 投稿日:2020-01-19T17:05:28+09:00
useReducerの本質:良いパフォーマンスのためのロジックとコンポーネント設計
React Hooksの正式リリース(2019年2月)からそろそろ一年が経とうとしています。Hooksの登場によってReactのコンポーネントは関数コンポーネントが一気に主流になり、クラスコンポーネントが新規に作られる機会は激減しました。
また、React 17.x系ではConcurrent Modeの導入とともにさらに2種類の新フックが追加される見込みであり、いよいよ関数コンポーネントの能力がクラスコンポーネントを真に上回る時代が来ることになります。
この記事では、フックの一種であるuseReducerに焦点を当てて、どのようなときに
useReducer
が適しているのかを説明します。究極的には、useReducerによって達成できるパフォーマンス改善があり、ときにはそれがコンポーネント設計にまで影響を与えることを指摘します。useStateの影に隠れたり、なぜかReduxと比較されたりといまいちぱっとしないuseReducerですが、この記事でその真の魅力を知っていただければ幸いです。
まとめ
useReducer
は、ステートに依存するロジックをステートに非依存な関数オブジェクト(dispatch
)で表現することができる点が本質である。- このことは
React.memo
によるパフォーマンス改善につながる。useReducer
を活かすには、ステートを一つにまとめることで、ロジックをなるべくreducerに詰め込む。背景:
useReducer
とはまずは、初心者の方向けに
useReducer
の動作を説明します。すでに知っているという方は次の節まで飛ばしても構いません。
useReducer
はフックの一種であり、関数コンポーネントのステートを宣言する能力を持ちます。ステートの宣言はuseState
とuseReducer
の2種類の方法がありますが、useReducer
は複雑なロジックが絡んだステートを宣言するのに適しています。
useReducer
は以下のように使います。こちらが用意するのはreducer
とinitialState
の2つです。reducer
は「現在のステート」と「アクション」を受け取って「新しいステート」を返す関数であり、initialState
はステートの初期値です。const [currentState, dispatch] = useReducer(reducer, initialState);
reducer
の返り値は2つで、currentState
はステートの現在の値、dispatch
はアクションを発火する関数です。dispatch
にアクションを渡すと、内部でreducer
が呼び出されて新しいステートが計算され、コンポーネントが再レンダリングされて新しいステートが反映されます。一応簡単な例を示しておきます。まずは
reducer
の例です。分かりやすさのためにTypeScriptを用いています。type State = { count: number }; type Action = { type: "increment" | "decrement"; }; const reducer = (state: State, action: Action): State => { if (action.type === "increment") { return { count: state.count + 1 }; } else { return { count: state.count - 1 } } };ここではアクションは
{ type: "increment" }
または{ type: "decrement" }
です。見て分かる通り、これはそれぞれ「カウンタを1増やす」操作と「カウンタを1減らす」操作に相当します。このreducerによって管理されるState
は{ count: number }
です。つまり、カウンタの数値をひとつ持っているだけのオブジェクトです。この場合type State = number
でも別に構いませんが、今後の拡張性を考えてこの定義にしています。これは
const [state, dispatch] = useReducer(reducer, { count: 0 })
のように使用します。このdispatch
を用いて、dispatch({ type: "increment" })
とすればステートが変化してカウンタの値が1増えるでしょう。これが
useReducer
の使い方です。useReducer
は、ステートの種類が増えたりロジックが増えたりしてもその操作の窓口がdispatch
という一点に集約されている点がポイントです。子コンポーネントが何かしらのロジックを発火したいときはdispatch
をpropsで渡すだけでいいし、コンポーネントツリーが大きい場合はコンテキストを用いて子に伝えるのも有効でしょう。
useReducer
がパフォーマンス改善につながる例Reactアプリのパフォーマンス改善において大きな効果が出やすいのは
React.memo
の活用です(クラスコンポーネント時代のshouldComponentUpdate
やPureComponent
に相当)。これを活用してコンポーネントの余計な再レンダリングを避けることが、Reactアプリの基本的なパフォーマンス・チューニングです。この例では、
useReducer
がReact.memo
の利用の助けになる例を示し、丁寧に解説します。初期状態のサンプル
まず、改善前の初期状態を見てみましょう。以下のCodeSandboxで実際に動作を確かめることができます。初期状態のコードは
App1.tsx
に入っています。今回の題材はこの画像のようなものです。
4つの入力欄があり、それぞれに数値を入力することができます。下には4つの数値を合計した値が表示されます。また、入力欄の横にある「check」ボタンを押すと、そのときの数値が合計の何%かを一番下に表示します。画像は「123」の横のボタンを押したあとの状態です。
一見意味不明な例に見えますが、これは実は筆者が実際に業務で経験した例をかなり単純化したものになっています。
この記事にも初期状態のコードを一気に貼り付けます。記事を読みつつコードを見たいという方は適宜CodeSandboxをご活用ください。記事中でも部分ごとに解説していきますから、ここで全部読む必要はありません。
src/App1.tsximport React, { useState } from "react"; import { sum } from "./util"; import "./styles.css"; const NumberInput: React.FC<{ value: string; onChange: (value: string) => void; onCheck: () => void; }> = ({ value, onChange, onCheck }) => { return ( <p> <input type="number" value={value} onChange={e => onChange(e.currentTarget.value)} /> <button onClick={onCheck}>check</button> </p> ); }; export default function App1() { const [values, setValues] = useState(["0", "0", "0", "0"]); const [message, setMessage] = useState(""); return ( <div className="App"> {values.map((value, i) => { return ( <NumberInput key={i} value={value} onChange={v => setValues(current => { const result = [...current]; result[i] = v; return result; }) } onCheck={() => { const total = sum(values); const ratio = Number(value) / total; setMessage( `${value}は${total}の${(ratio * 100).toFixed(1)}%です` ); }} /> ); })} <p>合計は{sum(values)}</p> <p>{message}</p> </div> ); }コードの解説
上記のサンプルのコードを少しずつ解説します。
まず、ひとつの入力欄とボタンのセットが、以下に抜粋する
NumberInput
コンポーネントで表現されています。入力状態は親のApp1
コンポーネントが持つvalues
ステートに保存されており、NumberInput
自体はステートを持っていません。現在の値はvalue
としてpropsを通じて渡されています。これは、「合計を表示する」といったロジックが親コンポーネントにあることから来る必然的な選択です。src/App1.tsx(抜粋)const NumberInput: React.FC<{ value: string; onChange: (value: string) => void; onCheck: () => void; }> = ({ value, onChange, onCheck }) => { return ( <p> <input type="number" value={value} onChange={e => onChange(e.currentTarget.value)} /> <button onClick={onCheck}>check</button> </p> ); };親コンポーネントである
App
は2つの状態を持ちます。以下に示すvalues
とmessage
です。src/App1.tsx(抜粋)const [values, setValues] = useState(["0", "0", "0", "0"]); const [message, setMessage] = useState("");
values
は4つの入力欄の内容が配列で入っています。message
は「check」ボタンを押したときに表示されるメッセージを管理するステートです。数値の入力が想定されていますが、ステートを数値にしてしまうとちょっと扱いづらいフォームになるので生の入力状態は文字列で持っています。あるあるですね。ステートの更新部分は
NumberInput
のpropsに渡す関数にベタ書きです。onChange
が呼び出されたら、setValues
を呼び出してi
番目の値がv
に書き換えた新しい配列を用意してステートを更新します。onCheck
も同様に、メッセージを組み立ててsetMessage
を呼び出します。src/App1.tsx(抜粋)onChange={v => setValues(current => { const result = [...current]; result[i] = v; return result; }) } onCheck={() => { const total = sum(values); const ratio = Number(value) / total; setMessage( `${value}は${total}の${(ratio * 100).toFixed(1)}%です` ); }}以上のコードの問題点は、レンダリングのパフォーマンス最適化が何も考えられていないことです。ひとつの数値が変更されるたびに全ての
NumberInput
に再レンダリングが発生してしまいます。今回のゴールは、
NumberInput
にReact.memo
を適用して無駄な再レンダリングを減らすことです。特に、ひとつの数値が変更されたらそのNumberInput
だけが再レンダリングされて、他のNumberInput
は再レンダリングされないという状態が理想です。お察しの通り、最終的には
useReducer
を用いてこれを達成することになります。
React.memo
導入への努力とりあえず、まずは
useState
のまま努力してみましょう。NumberInput
にReact.memo
を適用して効果を得るためには、他の入力値が変わってもpropsの内容が変化しないようにしなければいけません。現状ではvalue
は問題ありませんが、onChange
とonCheck
が問題です。あの位置に関数をベタ書きということは、これらのpropsには毎回異なる関数オブジェクトが作られて渡されています。これではReact.memo
は効きません。こういうときの定石は
useCallback
です。とはいえ、今回はループでNumberInput
を表示しているのでひと工夫必要です。筋のいい方法としては、NumberInput
に「自分が何番目か」を表すpropsを渡すという方法があります1。これをコールバックに渡してもらうことで、onChange
とonCheck
は全てのNumberInput
からのコールバックをひとつの関数で対応できます。以上の工夫を導入して得られたのが、上記のCodeSandboxでいう
App2.tsx
です。全体像を見たいからはCodeSandboxをご覧ください。ここでは部分ごとに変更点を見ていきます。まず
NumberInput
です。src/App2.tsx(抜粋)const NumberInput: React.FC<{ value: string; index: number; onChange: (index: number, value: string) => void; onCheck: (index: number) => void; }> = memo(({ value, index, onChange, onCheck }) => { return ( <p> <input type="number" value={value} onChange={e => onChange(index, e.currentTarget.value)} /> <button onClick={() => onCheck(index)}>check</button> </p> ); });
NumberInput
はpropsとしてindex
を受け取るようになりました。これが、自身が何番目かを表す数値です。onChange
とonCheck
の型も変更され、これらの関数にはindex
がオウム返しで渡されるようになっています。先ほども説明した通り、これによりonChange
とonCheck
を各NumberInput
ごとに異なる関数を用意する必要が無くなります。次に、
App
の変更点を見ます。まずレンダリング部分だけ抜粋すると、こうなりました。src/App2.tsx(抜粋)return ( <div className="App"> {values.map((value, i) => { return ( <NumberInput key={i} index={i} value={value} onChange={onChange} onCheck={onCheck} /> ); })} <p>合計は{sum(values)}</p> <p>{message}</p> </div> );
NumberInput
に渡すpropsにindex
が追加されているのに加え、onChange
とonCheck
が事前に用意されるようになりました。次に、これらを用意する部分のコードです。src/App2.tsx(抜粋)export default function App() { const [values, setValues] = useState(["0", "0", "0", "0"]); const [message, setMessage] = useState(""); const onChange = useCallback((index: number, value: string) => { setValues(values => { const newValues = [...values]; newValues[index] = value; return newValues; }); }, []); const onCheck = useCallback( (index: number) => { const total = sum(values); const ratio = Number(values[index]) / total; setMessage( `${values[index]}は${total}の${(ratio * 100).toFixed(1)}%です` ); }, [values] ); return /* 省略 */ }
onChange
とonCheck
はuseCallback
に囲まれています。それぞれの関数の中身は、index
を引数で受け取るようになった以外は変わりません。できることは全部やったように見えますが、残念ながらこのコードはまだ目的を達成できていません。
onChange
はuseCallback
により常に同じ関数オブジェクトになっているのでOKですが、onCheck
が問題です。
onCheck
はuseCallback
の第二引数が[values]
となっています。これは、values
が変わるたびに、すなわち何か入力が変わるたびに、onCheck
が作りなおされるということを意味します。これによりNumberInput
に渡されるonCheck
関数が毎回別物になるため、React.memo
が無意味になっています。では、なぜ
useCallback
の第二引数がvalues
を含んでいなければいけないのでしょうか。それはもちろん、onCheck
がvalues
に依存しているからです。つまり、onCheck
が中で「入力値の合計」を求めるためにvalues
を使用しているのです。onCheck
のインターフェースが(index: number) => void
である、すなわちindex
のみを受け取るという関数である以上、values
というデータについてはonCheck
に内包されていなければいけません。これにより、必然的にvalues
が変わるたびにonCheck
という関数は別物になります。一方で、
onChange
はvalues
に依存していません。これは、useState
が提供するステート更新関数が、関数によるステートの更新をサポートしているからです。上のコードではsetValues
関数の引数として「現在の状態を受け取って次の状態を返す関数」を渡しています。この機能により、onChange
からvalues
への依存を消しているのです。となると、
onCheck
がmessage
というステートを更新するにあたって、それとは別のvalues
というステートに依存していることが問題だと分かります。これを解消するためには、2つのステートを合体させて1つのステートにする必要があります。このような状況に適しているのが
useReducer
です。ということで、App
をuseReducer
を用いて書き換えることで問題を解決しましょう。(一応、useState
を使っていても2つのステートをまとめて問題を解決することはできますが、その状況でわざわざuseReducer
ではなくuseState
を使う意味は薄いのでここでは考えません。)
useReducer
による解決ということで、最終版です。全体像は以下のCodeSandboxの
App3.tsx
でご覧ください。まず、
useReducer
を使うのでreducerを用意しましょう。今回何気なくTypeScriptを使っているので型定義もちゃんとあります。src/App3.tsx(抜粋)type State = { values: string[]; message: string; }; type Action = | { type: "input"; index: number; value: string; } | { type: "check"; index: number; }; const reducer = (state: State, action: Action) => { switch (action.type) { case "input": { const newValues = [...state.values]; newValues[action.index] = action.value; return { ...state, values: newValues }; } case "check": { const total = sum(state.values); const ratio = Number(state.values[action.index]) / total; return { ...state, message: `${state.values[action.index]}は${total}の${( ratio * 100 ).toFixed(1)}%です` }; } } };型定義を読むと、
State
はvalues
とmessage
をひとつにまとめたオブジェクトであることが分かります。アクションは"input"
と"check"
の2種類があり、それぞれ前回のコードのonChange
とonCheck
に相当するロジックが書かれています。次に
NumberInput
のコードです。src/App3.tsx(抜粋)const NumberInput: React.FC<{ value: string; index: number; dispatch: Dispatch<Action>; }> = memo(({ value, index, dispatch }) => { return ( <p> <input type="number" value={value} onChange={e => dispatch({ type: "input", index, value: e.currentTarget.value }) } /> <button onClick={() => dispatch({ type: "check", index }) } > check </button> </p> ); });propsとして受け取るのは
value
,index
,dispatch
になりました。従来のonChange
とonCheck
がひとつにまとまっていますね。それ以外は特に変わっていません。最後に
App
コンポーネントのコードです。ロジックがreducer
の中に移ったのでこちらはシンプルになりました。src/App3.tsx(抜粋)export default function App() { const [{ values, message }, dispatch] = useReducer(reducer, { values: ["0", "0", "0", "0"], message: "" }); return ( <div className="App"> {values.map((value, i) => { return ( <NumberInput key={i} index={i} value={value} dispatch={dispatch} /> ); })} <p>合計は{sum(values)}</p> <p>{message}</p> </div> ); }ステートの宣言は
useReducer
により行われています。従来onChange
とonCheck
が担っていたロジックはreducer
の中に押し込められましたので、ここでは何もせずにただNumberInput
にdispatch
を渡すだけになっています。前のコードと比べると、ここに本質的なポイントがあります。それは2つのステートがひとつの
useReducer
に押し込められたことにより、「values
を見てmessage
を決める」という計算が「今のステートから次のステートを計算する」という枠組み(reducer)の中に入ったことです。よって、それを呼び出す側であるdispatch
はステートに非依存の関数となりました。NumberInput
のpropsはindex
,value
,dispatch
だけとなり、自分以外の値が変わっても再レンダリングされることは無くなりました。これで目標達成です。ポイントの整理
改めてポイントを整理すると、今回の最も重要だったことは「ステートの更新関数をステートに非依存にする」ということでした。
useReducer
の場合は、更新関数(dispatch
)が非依存であることが保証されています。従来のコード(2番目の例)ではonCheck
という関数がステート(values
)に依存している関数だったのでうまくいきませんでした。ステートの更新関数をステートに非依存にするには、「現在のステートを受け取って次のステートを計算する」ということを徹底する必要がありました。
useState
の場合はステート更新関数に関数を渡すのを徹底することになります。つまりsetValues(newValues)
ではなくsetValues(currentValues => {...; return newValues })
とするということです。従来のコードではonChange
ではこれができていましたが、onCheck
ではできていませんでした。これを改善するために今回行なったことは「2つのステート(
values
とmessage
)を1つに合体させる」ということです。これにより、onCheck
でも関数によるステート更新ができるようになりました。実を言えばuseState
でも頑張ればこれは達成できますが、このような複雑なステートを扱うにはuseReducer
が適しているのでここではuseReducer
を選択することになります。useReducer
を使う場合はステート更新関数(dispatch
)は自動的にステートに非依存になります(reducer
はそもそも「現在のステートを受け取って次のステートを計算する」というものであるため)。
useReducer
のすすめこのように、
useReducer
を用いることで、ステート更新関数をステート非依存にすることを強制できます。実際のアプリ開発においては、アプリが複雑化するにつれて、あるステートと別のステートが関わりを持ち始めるかもしれません。もっと具体的に言えば、あるステートを更新するときに別のステートを見る必要が発生するかもしれません。そのときがuseReducer
導入のサインです。ぜひリファクタリングしてuseReducer
を導入しましょう。なぜ
useReducer
が必要なのか、この記事を読んだ皆さんはしっかりと説明できることでしょう。ステート更新関数がステートに非依存であることは、React.memo
の活用には必須だからです。また、
useReducer
とReact.memo
の恩恵を最大限受けるためには、できるだけreducer
にロジックを詰め込むことが鍵となります。そのためには、アプリの状態は何でもステートで表現することが重要です。言い換えれば、これは手続き的なロジックを書かず、状態は明示的・宣言的に扱うということです。また、
useReducer
を活かすためにはそのためのコンポーネント設計も重要です。今回の例では多少天下り的でしたが、NumberInput
がindex
をpropsで受け取るようにしたという点にこれが表れています。dispatch
を呼び出して自分のvalue
を更新するためには自分が何番目かをdispatch
に教える必要があるからです("input"
アクションがindex
を含んでいたことを思い出しましょう)。副作用はどうするのか? あとReduxの話
ところで、今回の例では「check」ボタンを押すと起こることが「別のステートが更新される」でした。なので、
useReducer
によってステートをひとつにまとめることで、onCheck
コールバックをステートに非依存にすることができたのでした。では、もし「check」ボタンを押すと起こることが何らかの副作用だったらどうするのでしょうか。例えば、押すとHTTPリクエストが発生するとかです。現時点では、副作用はreducerの中に書くべきではないという原則がありますから、この記事で使った手を使うことはできません。
残念なことに、現時点では対処法はありません。副作用をどこかのコールバック関数に書いた時点で、その関数がステートに依存することとなり、
React.memo
によるパフォーマンス改善の妨げになります。実は、これに対する一つの解がReduxの使用です。Reduxを用いたステート管理の場合、Reduxミドルウェアの活用によって、ステートに依存する副作用ですら
dispatch
の中に押し込めてステート非依存性を達成できてしまうのです。Reduxの本質はReactのツリーの外でステートを管理してくれることであり、それによりReact本体のみでは困難なステート非依存性が実現しているのです。Reduxはただステート管理に関する統一的な方法論を与えるだけでなく、このようなパフォーマンス上のメリットもあるということは覚えておいて損はないでしょう。React 17.x 系の展望
しかし、React 17.x系(いわゆるConcurrent Modeが導入されると期待されています)ではまた情勢が変わると筆者は期待しています。端的に言えば、Concurrent Modeにおいては(主に非同期的な)副作用ですらステート内で管理されるようになるでしょう。そのための道具がSuspenseです。詳細はそのうち別の記事でお届けしようと思いますが、Concurrent Modeでは副作用とステート管理の概念が大きく様変わりし、Reduxなどに頼らずともパフォーマンス的に最適な副作用の扱いが達成できる場面が増えると予期されます。
useRef
に関する注意ところで、「コールバック関数がステートに依存するのが問題」ということであれば、
useRef
で解決できると思った方も多いでしょう。実際、以下のようにすればonCheck
をvalues
に非依存にすることができます。src/App4_useRef.tsx(抜粋)const [values, setValues] = useState(["0", "0", "0", "0"]); const [message, setMessage] = useState(""); const valuesRef = useRef<string[]>([]); valuesRef.current = values; const onCheck = useCallback((index: number) => { const values = valuesRef.current; const total = sum(values); const ratio = Number(values[index]) / total; setMessage(`${values[index]}は${total}の${(ratio * 100).toFixed(1)}%です`); }, []);この例では
values
の値はつねにvaluesRef.current
に反映され、onCheck
はvalues
を参照するかわりにvaluesRef.current
を参照するようにしています。useRef
が返すvaluesRef
は常に同じオブジェクトであることが保証されていますから、onCheck
はvaluesRef
に依存することはありません。この方法でもReact.memo
を活用するという目的は達成できています。しかし、筆者はこの方法はお勧めしません。なぜなら、このように
useRef
を使うのはReact 17.x系でうまく動作しなくなる可能性があるからです。Concurrent Modeにおいては、refへの書き込みはもはや副作用と見なされます。関数コンポーネントの処理中にこのようにrefへの書き込みを行うのは思わぬ動作を引き起こす可能性があるのです(特にレンダリングが中断される場合)。このことは実はReactの公式ドキュメントにも明記されています。「将来的にはより使いやすい代替手段を提供することを計画しています」とありますので、React 17.x系ではよりよい別の手段が提供されるかもしれません。
まとめ
この記事では、コールバック関数がステートに依存する場合に、
React.memo
の恩恵を受けられないという問題に対してuseReducer
を用いて対処する方法を示しました。ポイントはステート更新関数をステート非依存にすることであり、(useState
でもそれは可能なものの)useReducer
はそのような書き方に適しています。記事冒頭のまとめを再掲しておきます。
useReducer
は、ステートに依存するロジックをステートに非依存な関数オブジェクト(dispatch
)で表現することができる点が本質である。- このことは
React.memo
によるパフォーマンス改善につながる。useReducer
を活かすには、ステートを一つにまとめることで、ロジックをなるべくreducerに詰め込む。
useReducer
は素のReactでもreducerを使いたいというReduxかぶれの人のためだけに用意された機能では決してなく、このような本質的な問題を解決するための優れた道具なのです。
useState
に比べると使い方がややこしいので尻込みしてしまうかもしれませんが、useState
を多く並べれば並べるほど、いざ必要になったときのリファクタリングが難しくなります。時期を見極めてuseReducer
を導入しましょう。
ややアクロバットな別解として、
useMemo
を用いて各NumberInput
用のコールバックを用意するというものもあります。 ↩
- 投稿日:2020-01-19T16:08:15+09:00
【Vue.js】Vueコンストラクタ関数をWebコンソール上で見つける方法
Vue コンストラクタ関数とは?
Vue.js を webpack などのバンドラと共に使用している方にとっては、この
Vue
のことです:import Vue from 'vue'; ^^^
<script>
タグを貼り付けるだけのいわゆる「CDN版」の Vue.js を利用している場合はwindow.Vue
で簡単に Vue コンストラクタ関数を参照できますが、バンドラを用いてビルドされている場合においても Vue コンストラクタ関数を参照できる方法を紹介します。Vue コンストラクタ関数の参照を得る方法
以下のスクリプトをWebコンソール上で実行することで、Vue コンストラクタ関数を参照することができます:
const Vue = (() => { const el = [].find.call(document.all, el => el.__vue__); if (!el) { return; } let Vue = Object.getPrototypeOf(el.__vue__).constructor; while (Vue.super) { Vue = Vue.super; } return Vue; })();Vue 公式のブラウザ拡張である Vue.js Devtools の実装を参考にしました。
Internet Explorer 11 に対応した実装が必要であれば、こちらの Gist を参照してください。応用
プロダクションビルドされていても Vue.js Devtools を有効にする
2019年6月に Apple の SwiftUI のチュートリアルサイトが Vue.js で構築されていることが話題になりましたが、プロダクションビルドされたものにも関わらず Vue.js Devtools が有効となっていることにも驚いた方が多かったようです。
.@Apple is using @vuejs pic.twitter.com/RGZ6TIWjMj
— Rahul Kadyan (@znck0) June 3, 2019
Add debugger before new Vue() then set Vue.config.devtools = true and continue!
— Rahul Kadyan (@znck0) June 3, 2019
You have to find new Vue() in minimised code. Here search fornew n["a"]
.
この Twitter のやりとりをご覧になった @mottox2 さんがブレークポイントを挿入して Vue コンストラクタ関数を見つける方法1を紹介されていましたが、やり方を変えたものがこちらのスクリプトとなります:
(() => { if (!__VUE_DEVTOOLS_GLOBAL_HOOK__) { return; } const el = [].find.call(document.all, el => el.__vue__); if (!el) { return; } let Vue = Object.getPrototypeOf(el.__vue__).constructor; while (Vue.super) { Vue = Vue.super; } Vue.config.devtools = true; __VUE_DEVTOOLS_GLOBAL_HOOK__.Vue = Vue; })();上記スクリプトをコンソールで実行後、デベロッパーツールを開き直すと "Vue" タブが出現します。
バージョン情報の取得・ランタイム限定ビルドであるか判定する
ランタイムビルド・完全ビルドについて詳しくはVue.js 公式ガイドの「さまざまなビルドについて」を参照してください。
(() => { const el = [].find.call(document.all, el => el.__vue__); if (!el) { return; } let Vue = Object.getPrototypeOf(el.__vue__).constructor; while (Vue.super) { Vue = Vue.super; } console.log('Vue.version:', Vue.version); console.log('Vue.compile:', Vue.compile); })();ランタイム限定ビルドの場合は、
Vue.compile
がundefined
となっています:
完全ビルドの場合は、
Vue.compile
が定義されています:
状態を変更したり特定のページに遷移したりする
Vue コンストラクタ関数を見つける方法と近い方法で ViewModel を見つけることもできます2。
ViewModel を見つければ、特定の状態に変更したり、特定の画面に遷移することなどが可能です:(async () => { const $root = [].find.call(document.all, el => el.__vue__).__vue__.$root; await $root.$store.dispatch('HOGE_ACTION'); // Vuex $root.$router.push(`/fuga/path`); // Vue Router })();投稿完了画面など、実際に操作して遷移させるのが面倒な画面の CSS を書いたりするときに便利かもしれません。
Vue.js Devtools を用いる方法もあります: 【Vue.js / Nuxt.js】 ブラウザのコンソールでVueオブジェクトを表示させるには - poyopoyoのブログ ↩
- 投稿日:2020-01-19T16:03:01+09:00
obniz-nobleでBLEスキャンをグラフ表示してみた
obniz-nobleは手軽に使えていい感じですね!
今回は、ブラウザからESP32にobniz-nobleで接続して、周辺のBLEデバイスのRSSIをグラフ表示してみます。
完成図はこんな感じです。ブラウザから開いたのち、obniz idを入力して、接続ボタンを押下すると、1秒間隔でRSSIを取得して時系列にグラフ表示してくれます。
本ツールを作成したのは、Androidでも同様のツールがあるのですが、最近AndroidのBLEスキャン間隔が定期的に長くなってしまって使いにくくなったためです。
また、ブラウザで見ると大きな画面で確認できるし、M5StickCであれば、BLEスキャナーを持ち歩くことができます。試しに動かせるようにGitHubに上げておきました。
https://github.com/poruruba/ble_scanner以下をブラウザから開くことができます。
https://poruruba.github.io/ble_scanner/(2020/1/19 修正)
・BLEスキャンロストの制御を追加しました。5秒間、RSSIの更新がなければ、ロストとみなすようにしました。
・グラフ上の表示しない点は、値としてNaNをすればよいようです。使うツール
・obniz-noble
https://github.com/obniz/obniz-noble
今回の主役です。ESP32のObnizOS搭載デバイスをBLEセントラルにできます。・Chart.js
https://www.chartjs.org/
Javascriptでグラフ表示するためのライブラリです。・chartjs-plugin-colorschemes
https://nagix.github.io/chartjs-plugin-colorschemes/
グラフのLineの色を適当に選んでくれるプラグインです。・Bootstrap(v3.4.1)
https://getbootstrap.com/docs/3.4/
超有名なCSS等を使ったWebフレームワークです。・Vue(v2.x)
https://jp.vuejs.org/index.html
Javascriptフレームワークです。データの双方向バインディングなどが特徴です。あとは、最新のobnizOSが書き込まれたESP32が手元にある前提です。私はM5StickCを使いました。
Javascriptソースコード
Javascriptのソースコードを載せちゃいます。
start.js'use strict'; //var vConsole = new VConsole(); var noble; var devices = []; const NUM_OF_DATA = 50; const UPDATE_INTERVAL = 1000; const LOST_INTERVAL = 5000; var timer = null; var vue_options = { el: "#top", data: { progress_title: '', obniz_id: '', device: null, num_of_data: NUM_OF_DATA, update_interval: UPDATE_INTERVAL, obniz_connected: false, }, computed: { }, methods: { obniz_connect: function(){ noble = obnizNoble(this.obniz_id); noble.on('stateChange', (state) => { if (state === 'poweredOn') { this.obniz_connected = true; noble.startScanning([], true); this.interval_change(); } else { this.obniz_connected = false; noble.stopScanning(); } }); noble.on('discover', (peripheral) => { var device = devices.find(item => item.peripheral.address == peripheral.address); if( device ){ // device.peripheral = peripheral; device.peripheral.rssi = peripheral.rssi; device.counter = 0; }else{ // var peri = peripheral; var peri = { address: peripheral.address, addressType: peripheral.addressType, connectable: peripheral.connectable, advertisement: { serviceUuids: peripheral.advertisement.serviceUuids, manufacturerData: peripheral.advertisement.manufacturerData, localName: peripheral.advertisement.localName, txPowerLevel: peripheral.advertisement.txPowerLevel, }, rssi: peripheral.rssi, }; devices.push({ peripheral: peri, display: "display", datasets: [], counter: 0, }); } }); }, interval_change: function(){ if( timer != null ){ clearTimeout(timer); timer = null; } timer = setInterval(() =>{ this.update_graph(); }, this.update_interval); }, update_graph(){ for( var i = 0 ; i < devices.length ; i++ ){ if( devices[i].counter * this.update_interval < LOST_INTERVAL ){ devices[i].datasets.unshift(devices[i].peripheral.rssi); devices[i].counter++; }else{ devices[i].datasets.unshift(NaN); } } var current_datasets = []; for( var i = 0 ; i < devices.length ; i++ ){ current_datasets.push({ label: devices[i].peripheral.advertisement.localName ? devices[i].peripheral.advertisement.localName : devices[i].peripheral.address, data: [], fill: false, hidden: devices[i].display != "display" }); } if( current_datasets.length > 0 ){ for( var i = 0 ; i < current_datasets.length ; i++ ){ for( var j = 0 ; j < this.num_of_data ; j++ ){ if( j > devices[i].datasets.length ){ current_datasets[i].data[this.num_of_data - 1 - j] = NaN; }else{ current_datasets[i].data[this.num_of_data - 1 - j] = devices[i].datasets[j]; } } } var labels = []; for( var i = 0 ; i < this.num_of_data ; i++ ){ labels.push(i - this.num_of_data + 1); } myChart.data.datasets = current_datasets; myChart.data.labels = labels; myChart.update(); } } }, created: function(){ }, mounted: function(){ proc_load(); } }; vue_add_methods(vue_options, methods_utils); var vue = new Vue( vue_options ); var ctx = $('#chart')[0].getContext('2d'); var myChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [] }, options: { animation: false, scales: { yAxes: [{ scaleLabel: { display: true, labelString: 'RSSI [dB]' } }] }, legend: { position: "bottom", onClick: function(e, item){ vue.device = devices[item.datasetIndex]; } }, plugins: { colorschemes: { scheme: 'brewer.Paired12' } } } });各関数の説明を付記しておきます。
・obniz_connect()
obnizと接続します。接続が完了すると、以下のコールバックが呼ばれます。
noble.on('stateChange', (state) => {
そこで、BLEスキャンを開始します。
すると、スキャンに引っかかったBLEデバイスが以下のコールバックで通知されるようになります。
noble.on('discover', (peripheral) => {
このコールバックの中で、BLEアドレスを見て、新しいデバイスであれば内部の配列に追加し、すでにある場合は、RSSI値を更新します。
・interval_change()
グラフの再描画の間隔を変更します。一番最初のグラフ再描画ルーチンの開始にも使います。・ update_graph()
内部のデバイス用の配列に格納しておいた各BLEデバイスの最新RSSI値を取り出し、内部の履歴用の配列に追加します。この時先頭に追加します。そして、履歴用の配列の先頭から指定された数分だけのデータを取り出し、グラフの再描画を行います。指定された数分に満たないデバイスは、NaNとして表示対象から外しています。
ちなみに、グラフのセットアップは、以下の部分です。
var myChart = new Chart(ctx, {
以下の指定は、凡例を選択したときに、そのBLEデバイスの詳細を表示させるためのものです。(対象BLEデバイスのグラフの非表示/表示の切り替えも可能です)
legend: { position: "bottom", onClick: function(e, item){ vue.device = devices[item.datasetIndex]; } },以下の部分は、今回お世話になったプラグインの指定です。
plugins: { colorschemes: { scheme: 'brewer.Paired12' } }HTMLソースコード
最後に、HTMLソースです。
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;"> <meta name="format-detection" content="telephone=no"> <meta name="msapplication-tap-highlight" content="no"> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <!-- Optional theme --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <!-- Latest compiled and minified JavaScript --> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <title>BLEスキャン</title> <script src="js/methods_utils.js"></script> <script src="js/vue_utils.js"></script> <script src="dist/js/vconsole.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <!-- <script src="https://unpkg.com/obniz/obniz.js"></script> <script src="https://unpkg.com/m5stickcjs/m5stickc.js"></script> --> <script src="https://unpkg.com/obniz-noble/obniz-noble.js" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js"></script> <script src="https://unpkg.com/chartjs-plugin-colorschemes"></script> </head> <body> <div id="top" class="container"> <h1>BLEスキャン</h1> <div class="form-inline"> <label>obniz id</label> <input type="text" class="form-control" v-model="obniz_id" v-bind:readonly="obniz_connected"> <button v-if="!obniz_connected" class="btn btn-default btn-sm" v-on:click="obniz_connect">接続</button> </div> <div class="form-inline"> <label>表示数</label> <select class="form-control input-sm" v-model.number="num_of_data"> <option value="10">10</option> <option value="20">20</option> <option value="50">50</option> <option value="100">100</option> </select> <label>更新間隔</label> <select class="form-control input-sm" v-model.number="update_interval" v-on:change="interval_change"> <option value="500">0.5s</option> <option value="1000">1s</option> <option value="5000">5s</option> <option value="10000">10s</option> <option value="60000">60s</option> </select> </div> <br> <canvas id="chart"></canvas> <br> <div v-if="device" class="panel panel-default"> <div class="panel-heading"> <div v-if="device.peripheral.advertisement.localName"> {{device.peripheral.advertisement.localName}} </div> <div v-else> {{device.peripheral.address}} </div> </div> <div class="panel-body"> <div class="form-inline"> <label>グラフ表示</label> <select class="form-control input-sm" v-model="device.display"> <option value="display">表示</option> <option value="hidden">非表示</option> </select> </div> <label>localName</label> {{device.peripheral.advertisement.localName}}<br> <label>address</label> {{device.peripheral.address}}<br> <label>RSSI</label> {{device.peripheral.rssi}}<br> <label>addressType</label> {{device.peripheral.addressType}}<br> <label>connectable</label> {{device.peripheral.connectable}}<br> <label>serviceUuids</label> {{device.peripheral.advertisement.serviceUuids}}<br> <div v-if="device.peripheral.advertisement.manufacturerData"> <label>manufacturerData</label> {{device.peripheral.advertisement.manufacturerData.toString('hex')}}<br> </div> <div v-if="device.peripheral.advertisement.txPowerLevel"> <label>txPowerLevel</label> {{device.peripheral.advertisement.txPowerLevel}}<br> </div> </div> </div> <br> <div class="modal fade" id="progress"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">{{progress_title}}</h4> </div> <div class="modal-body"> <center><progress max="100" /></center> </div> </div> </div> </div> </div> <script src="js/start.js"></script> </body>Vue.jsやBootstrapを使い倒しています。
その他、細かなファイルがありますが、GitHubをご参照ください。以上
- 投稿日:2020-01-19T15:43:30+09:00
Reactでの条件分岐 4つの方法のメモ
はじめに
Reactでの条件付きレンダリングの方法のメモです。
目次
- returnするJSXを分岐
- 変数にJSXを格納
- もう少し短く!!
- これで最後!!
- まとめ
1. returnするJSXを分岐
stateを条件に、判定してJSXをそのままreturnしてあげる方式
import React, { Component } from 'react' export default class Greeting extends Component { constructor(props) { super(props); this.state = { isMorning: true }; } render() { if (this.state.isMorning) { return <h2> Good Morning Tom</h2> } else { return <h2> Hye ! Tom </h2> } } }2. 変数にJSXを格納
変数にJSXを格納することで、returnの箇所が1つになりました。
import React, { Component } from 'react' export default class Greeting extends Component { constructor(props) { super(props); this.state = { isMorning: false }; } render() { let message if (this.state.isMorning) { message = <h2> Good Morning Tom</h2> } else { message = <h2> Hye ! Tom </h2> } return <div>{message}</div> } }3. もう少し短く!!
もう少し短く書けます
条件式 ? (trueの時) : (falseの時)
import React, { Component } from 'react' export default class Greeting extends Component { constructor(props) { super(props); this.state = { isMorning: true }; } render() { return (this.state.isMorning ?<h2> Good Morning Tom</h2> :<h2> Hye ! Tom </h2>) } }4.これで最後!!
falseの時に、何も表示させない時とかは
条件式 && (trueの時)
みたいに書ける条件式が評価されて、trueなら次に行くので
条件式がfalseなら次に行かずに何も返さないimport React, { Component } from 'react' export default class Greeting extends Component { constructor(props) { super(props); this.state = { isMorning: true }; } render() { return (this.state.isMorning && <h2> Good Morning Tom</h2>) } }5. まとめ
4つのパターンを見ましたが、最後の2つがシンプルかなと思います。
ありがとうございました。
- 投稿日:2020-01-19T15:34:45+09:00
FileMaker の DDR を見やすくする JavaScript
簡単な JavaScriptファイルをひとつ追加して、DDR(データベースデザインレポート)の可用性を向上させてみました。もちろん DDRを基にした FileMakerアプリ解析ツールは様々あるのですが、そこまでの大分析が目的ではなくて、WEBブラウザ利用はそのまま軽く閲覧と検索をしたいのだけれど、デザイン的な不便さを何とかしたい、という用途のものです。 このツールで変わる事 スクリプトステップの行番号を表示するようになるよ。 スクリプトステップ内の式や、カスタム関数が、コードそのまま表示になるよ。 スクロールしても、スクリプト名が常に表示されるので、迷子にならないよ。 前後のスクリプトへ移動するナビゲーションを除去したよ。 行番号を表示し、式や関数はそのまま表示 FileMakerのステップは行番号が表示される(Pro 14~)ものの、DDRではリストスタイルが標準の黒丸「list-style-type: disc」のため、分析時に何行目なのか即座に判らないままです。このスタイルを「list-style-type: decimal-leading-zero」に変え、行番号を出すようにしました。 また、ステップ内の式を、記述そのまま(<pre>)の表示にすることで、1ステップ1行に強制されず読み下ししやすくなり、素早い箇所の特定に役立つようになります。こっちもスタイル「white-space: pre」を追加しただけです。 before 行番号が判らず、計算式を指定されているステップ行では式が改行されておらず読めません...? スクリプト名も判り難いと思います。 after 行番号が判るだけでなく、オプションの計算式は記述したとおりに確認できるようになりました。? インポートステップなども、行展開されて表示されます。 before カスタム関数カタログも、これではどんな式なのか判らない...? after こうなります。? 引っ付いてくれば迷子にならない スタイル「position: sticky」を使って、スクロールしていても、スクリプト名などがウィンドウ上部に常に固定表示されるようにしました。 ※ Chrome、Firefox のみで機能します。 before 検索ハイライトされている箇所が、何というスクリプトの何行目なのか さっぱり判らない。? after スクロールダウンしていても、表示箇所のスクリプト名と行番号が判ります。? 前後のスクリプトと上位フォルダへの移動ナビゲーション除去 個人的にこのナビゲーションを機能的に利用したことがなく、何より WEBブラウザでスクリプト名で検索した時に余計に検知されるので、要素そのものを除去(jQuery「$(elements).remove()」)しました。 before このように、スクリプト名で検索した時に、余計にハイライトされ、検索結果数も正確でなくなってしまいます。? ダウンロード この リンク 先を保存してください。 使い方 【注意】 CDNでホストされている jQueryを参照しているので、インターネット接続が必要 です。 オフラインで利用する場合は、jQueryも別途ダウンロードして、代わりに参照させてください。 DOM操作だけを行うのが目的なので、jQueryでいいでしょう。 【手順】 DDRをHTMLで生成する。 Summary.html(概要.html)の階層に本JSファイルを置く。 各「{{FileMakerファイル名}}_ddr」サブフォルダの「{{FileMakerファイル名}}.html」 内の適当な位置に以下のスクリプトタグのリンクをつける。 あとは、ブラウザでページをリロードするだけです。 {{FileMakerファイル名}}.html ... <script src="../ddrfix.js"></script> </head> ... ご意見、改善点などございましたら、ぜひフィードバックをください。
- 投稿日:2020-01-19T14:18:57+09:00
ページ遷移先でリロードしないと非同期通信(ajax)できない
はじめに
某プログラミングスクールの課題で、Railsを使ってECサイトを作成しています。
エラー発生時の状況
トップページにて、link_to でページに移動するとリロードしないと非同期通信できない
エラーの仮説
turbolinksが邪魔してそう...
turbolinksってなんだっけ?
turbolinksとは、ページ遷移をAjaxに置き換え、JavaScriptやCSSのパースを省略することで高速化するgemで、Rails 4からはデフォルトで使用されるようになります。
原因
data-turbolinkが働いて遷移したページではイベントが発火しない事があるらしい.
対策
= link_to new_item_path,data: {"turbolinks" => false}, class: "seller_btn" doこれ→
{"turbolinks" => false},data-turbolinkをオフにする
遷移元のリンクタグにdata属性を追加
参考記事
Rails6でjqueryアニメーションライブラリanimsitionの使用 | 躓いたことなど...
https://qiita.com/lookatachic/items/cc3accb542fca0eaf43a