20200119のJavaScriptに関する記事は30件です。

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.js
function showDateTimePerSecond() {
   console.log(new Date());
};

document.getElementById('button').onclick = function(){
  setTimeout(showDateTimePerSecond,2000);
}

image.png

setTimeout(関数名,ミリ秒)で繰り返し関数を呼ぶ

setTimeoutもsetIntervalと似たような使い方ができます。
以下の例では、showDateTimePerSecondの関数内でsetTimeoutを呼び、引数にshowDateTimePerSecondを取ることで繰り返しの処理をしています。

index.js
function showDateTimePerSecond() {
  console.log(new Date());
  setTimeout(showDateTimePerSecond, 1000);
};

showDateTimePerSecond();

clearTimeout()で処理を止める

setTimeoutの繰り返しを止めるには、clearTimeoutというメソッドが用意されています。
以下の例では変数iが3になった時点でclearTimeoutを呼んで処理を止めています。

index.js
let 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で繰り返し処理 ← 呼び出しを行なった関数の処理が終わった後、指令された時間を置いて、引数で渡されている関数を呼ぶ。

https://ja.javascript.info/settimeout-setinterval

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js install for mac

何からNode.jsを入手したか忘れないためのメモ。

  • 公式からLTS版mac用のinstallerをダンロードする
  • installerを起動し、各設問をデフォルトのまま進めてinstallする
    • スクリーンショット 2020-01-19 23.05.14.png
  • nodeのバージョンを確認する
$ node -v
v12.14.1
  • npm(パッケージマネージャ)のバージョンを確認する
$ npm -v
6.13.4

以上です。

参考:
https://jsprimer.net/use-case/setup-local-env/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.ts

SharedModuleを作成

  • 共有コンポーネントを管理するSharedModuleを作成します
ng generate module shared
  • 生成されたshared.module.tsに共有したいCustomButtonの定義を追加します
    • declarationsとexportsの2か所に追加する必要があるので注意
  • 共有したいコンポーネントを新しく作るときは同じように追加していくことになります
src/app/shared/shared.module.ts
import { 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.ts
import { 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.ts
import { 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.ts
import { 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しないといけないのが個人的にはまりどころでした

参考ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Googleサービスでサーバーレスなカウントダウンサイトを作ってみた

完成イメージ

Googleアカウントがあれば、無料のサーバーレス運用ができます。
image.png
実際に作ったサイト
https://sites.google.com/view/olympic-paralympic/

さっそく作成

Googleサイトを利用します。
https://sites.google.com/new

右下のをクリック
image.png

※Googleドライブからも作成できます。
image.png

サイトが作成されます。
image.png

左上にあるサイトの名前を変更します。(任意)
image.png

テキストボックスを見出しに変更して、文字のバランスを整えます。
image.png

見出しのタイプをカバーに変更し、続いて画像を変更します。
image.png

表題を編集します。
オリンピック・パラリンピックの文字の後ろで改行して、TOKYO2020を入力後スタイルを題名に変更、もう一つ改行して2020年7月24日 午後8時 開幕式まであとを入力後スタイルを小見出しに変更します。
image.png
image.png

※写真は次のサイトの素材を利用させていただきました。
フリー写真素材ぱくたそ

カウントダウン作成

カウントダウンをJavaScriptを使って作成します。
画面右側の挿入から埋め込みをクリックします。
image.png

埋め込みコードを選択します。
image.png

ここに、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>

次へをクリックします。
image.png

フォントが白のため、真っ白に見えます。
信じて挿入をクリックします。
image.png

下に挿入されるので、先程作成した表題の下にドラッグアンドドロップします。
image.png

カウントダウンのフィールドを選択し、縮めるなどして調整します。
出来たら、右上の公開ボタンをクリックします。
image.png

最後にウェブへの公開設定をします。
希望のウェブアドレスを入力し、サイトを閲覧できるユーザー等設定後、公開ボタンをクリックします。
image.png

公開すると、右上の公開の横にある下三角ボタンをクリックするとサイトを確認することができます。
image.png

おわりに

以上、Googleサイトを利用して、簡単にカウントダウンサイトを作る方法でした。
Googleアカウントがあれば、だれでもそのまま利用できるので、とても便利です。
JavaScriptで動かすので、ディスプレイに映し出してサイネージとしても利用できるかと思います。一時的にとか、ちょこっと的なときに、少しでも役立っていただけたら嬉しく思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webGLでVertex Animation Textureをやる

WebGLでVertex Animation Textureを実験的にやってみた記事です。

環境

・Three.js
・gsap
・Houdini Indie 18
Houdiniの無料版でできるかは確認していません。
・Windows 10

Vertex Animation Texture について

VATとはそれぞれのピクセルに頂点の位置、回転などの情報を特定のフレーム分含めたテクスチャデータの事です。
テクスチャのサイズですが、ポイントの数、フレーム数が多くなるに連れてテクスチャサイズも大きくなります。
テクスチャのX軸が頂点番号になり、Y軸はフレーム数になります。

vertex_animation_textures1_col.png

HoudiniでVertex Animation Textureを出力する

Houdiniにはゲーム開発用の便利ツールがあります。
lab.png
node.png

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を使用した場合、カスタムアトリビュートも一緒にエクスポートしてくれます。

Desktop_houdini_pointToTexture_test.hiplc - Houdini Indie Limited-Commercial 18.0.348 2020_01_19 16_55_58.png

テクスチャデータについて

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
houdini_pointToTexture_test.hiplc - Houdini Indie Limited-Commercial 18.0.348 2020_01_19 17_06_32.png

実際に作って読み込んでアニメーションさせてみる

ブラウザで表示させる

今回はテスト用にてっとり速く、確認できるデータを作成します。
boxを作成し、適当に色をつけて、ポイントの番号をidにするvexを書いて、
適当にアニメーションをつけたものをgltfで出力し、ブラウザで表示します。
gltfで出力するとき、タイムラインを一番最初にしておく必要がある

f@id = @ptnum;
ptnumそのままいれるとint型なのでエラーになります。

houdini_VAT_test.hiplc - Houdini Indie Limited-Commercial 18.0.348 2020_01_19 17_40_26.png

kayac-html5-starter - Google Chrome 2020_01_19 18_23_08.png

Labs Vertex Animation Textureでtextureを出力させる

houdini outネットワークに入りLabs Vertex Animation Textureを使います。
今回がポジションをVATします。
一応カラーのVATも作成しておきます。

設定は変更箇所は太文字になってます。
・user interface -normalに変更
・engineをMantraに変更
・sop pathにつかうオブジェクトを選択

Desktop_houdini_VAT_test.hiplc - Houdini Indie Limited-Commercial 18.0.348 2020_01_19 20_23_19.png

Desktop_houdini_VAT_test.hiplc - Houdini Indie Limited-Commercial 18.0.348 2020_01_19 20_23_39.png

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_col.png

vertex_animation_textures1_pos.exr
vertex_animation_textures1_pos.png

これで準備完了です。

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.js
import {
    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.vs
precision 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.fs
precision 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に変換することでなぜ値がずれます。

名称未設![kayac-html5-starter---Google-Chrome-2020-01-19-22-19-16.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152896/9ba354bc-8a9c-312c-4478-363084b807c3.gif)<br>
定 2.jpg

名称未設定 1.jpg

これを解決するには
https://convertio.co/ja/exr-png/
というサービスを利用してexrをpngに変換した場合、うまくいきます。
しかしこれを読み込ませてもずれます。
これはexrデータを16bit pngをさらに8bit pngにすると何故かうまくいきます。
そして8bit pngに変換するのにphotoshopを使います。

名称未設定 3.jpg

これでうまくいきます。
ですが、32bitから8bitにしたことでかなり劣化します。
結果、頂点の位置がかなりぷるぷるします。
ズームするとよくわかります。
もともとゲーム開発などに使われる技術なので、web向けにするには
値を補完したり、pythonで独自にファイルを出力するコードをhoudiniに
書く必要がありそうです。

kayac-html5-starter---Google-Chrome-2020-01-19-21-00-56_2.gif

大量の頂点を動かす

houdini上でclothの物理演算を行いそれをブラウザで再現して見ます。
こんな感じになります。
たぶん精度の問題で破綻してる部分がありますが、一応再現できていると思います。

kayac-html5-starter---Google-Chrome-2020-01-19-21-58-29.gif

kayac-html5-starter---Google-Chrome-2020-01-19-22-19-16.gif

githubはこちら
https://github.com/machilda/houdini-to-three-vertex-animation

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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">&times;</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-19 222020.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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">&times;</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-19 222020.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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-20 12.32.32.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エンジニアのポートフォリオを作ってみた

以前からポートフォリオサイトを作ってみたかったので今回作ってみました。

https://sssttt-maker.github.io/portfolio/
image.png

github.ioで公開できる最近まで知りませんでした。

デザイン

デザインは、僕の大好きなブランド会社のTOKYO BASEさんのホームページを参考にさせていただきました。
http://www.tokyobase.co.jp/

パララックスサイトみたいなのがかっこいいなと思ったので導入。
セクションが次々に動いていてとても気持ちいいです。

cssで

position: sticky;

というのをsection指定にして入れるだけで簡単に再現できちゃいます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript勉強の記録その21:setIntervalとclerIntervalを使って関数の呼び出し

setIntervalを使って関数を呼び出す

setInterval(関数名,ミリ秒)とすることで、特定のメソッドを指定したミリ秒間隔で呼び続けることができます。 
以下の例では、showDateTimePerSecondという関数を1秒間隔で呼び、Dateオブジェクトを生成し、コンソールに表示しています。

index.js
function 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.js
let 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を止めているということです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.js
var 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)
    • 初心者にもわかりやすく説明されているため、オススメです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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_compatibility

url-search-params-polyfill

以下のpolyfillを追加して解消した。
jerrybendy/url-search-params-polyfill: a simple polyfill for javascript URLSearchParams

追加

npm i --save url-search-params-polyfill
main.js
import "url-search-params-polyfill";

参考

IE11 > axios > POSTのつもりがうまく送れてないとき
IE11でURLSearchParamsを使った処理でエラーが発生する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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: 70

map

{
  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);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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>

これで再生と停止ができたみたいです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon Prime VideoやAbema TVなどを画面端の小さいウィンドウで再生する方法

javascript:document.querySelector("video[src]").requestPictureInPicture();

をブックマークのURLに追加。

動画を再生してるときにブックマークを開く。

参考

https://qiita.com/saa/items/55411d5c0c7fd9bc8651

https://qiita.com/iHok/items/2a51a71c26c8abf820e0

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

micro:bitとブラウザ間で文字列をやり取りするまでの手順

目的

スマホ等からWebBluetoothを使って micro:bit に文字列を送信、
また、逆に micro:bit からスマホ等に文字列を送信するための手順をまとめています。


使用するもの

  • micro:bit
  • USB ケーブル
  • WebBluetooth 対応ブラウザ(2020年1月時点でPCまたはAndroidのChrome)
  • テキストエディタ

micro:bit 側のプログラム作成


まずは Bluetooth に接続、切断されたことがわかるようにしてみます。


https://makecode.microbit.org にアクセスします。
image.png


新しいプロジェクトを選択します。
image.png


右上の歯車をクリックし、プロジェクトの設定を選択します。
image.png
※日本語表示になっていない場合は、Language から、日本語を選択します。
image.png
image.png


名前を UART にし、No Pairing Required: Anyone can connect via Bluetooth. を有効にします。
image.png
※これで WebBluetoothから面倒な手続きなしで micro:bit に接続できるようになります。


ブロックプログラミングを始めます。
image.png


高度なブロックを開きます。
image.png


拡張機能を選択します。
image.png


bluetooth を選択します。
image.png


「一部の拡張機能を削除してbluetoothを追加する」ボタンを押します。
image.png


bluetooth - その他 のなかの「Bluetooth UARTサービス」を「最初だけ」ブロックの中に配置します。
image.png
image.png


接続されていない状態がわかるように、基本 - アイコン から目を閉じた顔マークを配置します。
image.png
image.png


Bluetooth が接続されたことがわかるように、Bluetooth のなかの 「Bluetooth 接続されたとき」と笑った顔マークを配置します。
image.png
image.png


Bluetooth が切断されたことがわかるように、「Bluetooth 接続が切断されたとき」と目を閉じた顔マークを配置します。(顔マークは先に配置したものを右クリックで複製すると早いです。)
image.png
image.png


作成したプログラムの書き込み


右上歯車ボタンを押し、「デバイスを接続する」を選択します。
image.png


さらに、「デバイスを接続する」を選択します。
image.png


次のようなポップアップが表示されるので、 micro:bit を選択して、「接続」ボタンを押します。
image.png
※これで、画面左下の「ダウンロード」ボタンを押すと、作成したプログラムが直接 micro:bit に書き込まれるようになります。


画面左下の「ダウンロード」ボタンを押し、プログラムを書き込みます。
image.png
書き込みが成功すれば microbit に顔マークが表示されます。
image.png

ブラウザ側のプログラム作成


テキストエディタを起動して、次の内容を入力します。

<!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 という名前で保存し、ブラウザで開きます。

image.png


「接続」ボタンを押すと、次のようなポップアップが表示されるので、 micro:bit を選択して、「ペア設定」ボタンを押します。
image.png


うまく行けば、micro:bit の顔の表示が変わり、ブラウザのボタンは切断ボタンに変わります。
image.png
image.png


切断ボタンを押すと、顔の表示が戻ります。
image.png


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 から変更された部分は次のとおりです。
image.png
image.png


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側のプログラム変更


「ボタンが押されたとき」のブロックを追加します。
image.png
image.png


「Bluetooth UART 文字列を書き出す」を追加します。
image.png
image.png


書き出す内容を、 Button A pressed. とします。
image.png


左下「ダウンロードボタン」を押して、micro:bit に変更したプログラムを書き込みます。


micro:bit → ブラウザ 文字送信動作確認


ブラウザで uart2.html を開きます。


接続ボタンを押します。


micro:bit の A ボタンを押します。


ブラウザの受信内容に、 Button A pressed. と表示されれば成功です。
image.png


ブラウザから micro:bit へ文字を送信する

microbit側のプログラム変更


「Bluetooth データを受信したとき」のブロックを追加します。
image.png
image.png


「文字列を表示」ブロックを追加します。
image.png
image.png


表示内容を、「Bluetooth UART つぎのいずれかの文字の手前まで読み取る」にします。
image.png
image.png


ブラウザ側のプログラム変更


次の内容で、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>

※ uart2.html からの変更箇所は次のとおりです。
image.png
image.png


service.getCharacteristic(UUID_RX_CHAR_CHARACTERISTIC) で micro:bit の受信機能のハンドルを取得し、
rx.writeValue でブラウザから文字列を送信しています。


ブラウザ → micro:bit 文字送信動作確認


ブラウザで uart3.html を開きます。
image.png


接続ボタンを押して接続します。
image.png
※接続が成功すれば、送信ボタンが押せるようになります。


送信ボタンを押します。成功すると、micro:bit に test という文字がスクロールで表示されます。
output.gif


以上で、ブラウザと micro:bit 間の文字列送信ができるようになりました。

これをベースに、ブラウザからコマンドを送って、micro:bit にそのコマンドを判別していろいろな動作をさせたり、
micro:bit からセンサーデータをブラウザに送ってブラウザ側でグラフにしたりすることができそうです。


おまけ


計測開始を押すと、micro:bitの光センサーの値が、1秒毎にブラウザに送られてくるプログラムを作ったので共有します。

ブラウザ側
https://programpark.jp/microbit_uart.html

micro:bit側
https://makecode.microbit.org/_gJP7Ab3K7hH9


おわりに

日本語は送受信できませんでした。
1度の送信は20文字までのようです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

最近関数言語に興味がある

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
import Enzyme, { shallow } from 'enzyme';
import EnzymeAdapter from 'enzyme-adapter-react-[version]';

Enzyme.configure( { adapter: new EnzymeAdapter() });

実際にテストする

テストするコンポーネント

App.js
import 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.js
test('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の動画を見終わっていないので、全て見たら改めて追加情報をまとめようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.2

nginxの起動

$ sudo service nginx start

nginxへの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アプリが起動していないためです。
bad_gateway.png

reactアプリの作成

nodejsのインストール(インストール済なら不要)

$ curl --silent --location https://rpm.nodesource.com/setup_10.x | sudo bash -
$ sudo yum -y install nodejs

reactアプリの作成

$ 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へのバージョンダウン)を行ってください。

?ブラウザで確認

ブラウザからhttps接続して確認すると、以下のページが表示されます。
hello_react.png
以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】useReducer をもっと自由に活用しよう

useReducer をもっと自由に

皆さん、useReducer は活用していますか?
useState で十分と思っている場合でも useReducer に置き換えることで、コードがシンプルかつ、わかりやすくなります。

そのためには、Reduxの呪縛を解き払ってください。useReducerに Action type は必要ないですし、Flux Standard Action も必要ありません。また、今回はdispatchdispatchらしい使い方はしていません。
つまり、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) を、useStateuseReducer でそれぞれ作成します。

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 に置き換えることで、コードがシンプルかつ、わかりやすくなり、テスタビリティがあがってパフォーマンスも上がります!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptのspliceで削除すな

概要

Backendで動くJavascriptのコードを引き継いだ際に、ちょっとやばそうなミスをしていたので共有します。

環境

OS:Windows 10
言語:Javascript

splice関数?

Javascriptの配列標準関数。
大体の使い方は以下を参照。
spliceは切り取るだけのメソッドではなかった

問題箇所

問題の箇所は以下。
特定の形のエラーデータをspliceで削除しようとしていた。

// ErrorData削除処理
for(var i=0; i<array.length; i++){
  if(checkErrorData(array[i]===true)){
    array.splice(i, 1);  // i番目の要素を削除
  }
}

一見問題なさげに見えるけど大問題。
splice関数は配列のインデックスをめちゃくちゃにするので一連のループ処理で何度も実行するとえらいこっちゃになる。

ちょっと掘り下げ

実例で考える。
配列初期.001.jpeg

Errorデータを削除するため、i=3時に削除が実行されます。
配列初期.002.jpeg

この時、削除された段階で配列が作り替えられています。forループのカウンタiはそのままインクリメントされるため、元々index=4だった要素に対してErrorチェックが行われません。

配列初期.003.jpeg

問題まとめ

Errorデータの次のデータにチェックが行われない構造になっていますね。
なので以下のデータで削除漏れが起こります。
配列初期.004.jpeg

対策

  1. splice関数を使ったお姉さんに文句をいいましょう
  2. 配列から要素を削除する際はfilter関数を使いましょう
  • filter関数は以下を参考

【JavaScript入門】filterで配列のデータを抽出する方法

削除というよりはErrorデータ抜きの新しい配列を代入する感じです。
速度的にも圧倒的にこっちの方が早いですね。
以下に書き換えました。

// ErrorData削除処理
array = array.filter( function (item) {
  return checkErrorData(item) === false;
})

callback関数でitem(配列の各要素)をチェックして、正常なデータの時だけtrue値を返す感じです。

以上、駄文でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 PATH

Arch 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.js
export function greet(name) {
  console.log(`Hello, ${name}!`);
}
index.js
import { 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 と引数(個数と配列先頭のポインタ)を受け取って値を返す関数
        • JSValueConstJS_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_NewObjectClassJS_GetOpaque / JS_SetOpaque。このようにして std::mt19937 * の生存期間の管理を GC に任せることができる。

モジュールを利用する

モジュールの import / export を行いたい場合、JS_Eval 時に JS_EVAL_TYPE_MODULE フラグが必要となる。このフラグが有効な場合、戻り値 retJS_UNDEFINEDJS_EXCEPTION のいずれかになる。

    JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT);

qjs コマンドのようにファイルシステムから JS ファイルをモジュールとしてロードしたい場合、 quickjs-libc.cjs_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.cjs_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 というラッパーがあるがやや機能が不足しているので自分でも実装したりしなかったりしていきたい。

参考情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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は無効にしています)

コードの記載にあたっては、次のような簡易アプリを作って、挙動を確認しています。
スクリーンショット 2020-01-18 15.43.03.png
ビューファイルは、次のように記載しています。

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.rb
class 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
end

2-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.log
Started 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:のところがセキュリティトークンに当たります。

RequestHeaders
POST /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.log
Started 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:とあり、セキュリティトークンが付加されています。

RequestHeaders
POST /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.js
window.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.js
let html = `<div class="comment">
              <p class="comment_name">投稿者:山田</p>
              こんにちは!
            </div>`

これと同じ内容を、JavaScriptで記述すると、次のようになります。

sample.js
    let 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.js
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メソッドでサーバに送信

サーバにリクエストを送信するために使用しているオブジェクト及びメソッドは、次のとおりです。

項番 項目 内容
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 / XMLHttpRequest

2-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.js
xmlHR.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.js
let 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.js
let 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.js
xmlHR.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.js
if (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.js
window.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.js
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(); //サーバに送信

異なると言えば、send()メソッドの送信時に、引数としてデータを指定していないということくらいでしょうか。

削除時のログは、次のようになっています。

log/development.log
Started 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.js
let 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.js
aElm.addEventListener("click", function(e) { deleteHTMLEvent(e, aElm) }, false); //ここが大事!

新規投稿のHTMLが生成がされる度に、上記の1行でaタグに新しいイベントリスナーを追加するようにしています。

ここでコールバック関数として呼び出されているのはfunction(e) { deleteHTMLEvent(e, aElm) }の部分です(コールバック関数に引数を渡す方法については、「Javascriptでイベントハンドラのコールバック関数に引数を渡す」を参照)。

呼びだされるコールバック関数は、最初にページをロードする際に読み込む関数と同じもので、具体的には、次の部分となります。

sample.js
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);
      }
    }
  };
};

新規の投稿がページに追加される度に、上記の関数が呼び出されて、イベントリスナーとして追加登録されているということになります。

●参考サイト
[JavaScript] イベント処理を動的に追加する
動的に追加した要素にaddEventListnerを設定する方法

4. おわりに

まとめておきたかった内容は、以上のところです。

挙動は確認しているので、動作はするはずですが、もっと良い書き方や、正しい書き方があるのだろうと思います。
お気付きのことがあれば、ご指摘等をいただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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はフックの一種であり、関数コンポーネントのステートを宣言する能力を持ちます。ステートの宣言はuseStateuseReducerの2種類の方法がありますが、useReducerは複雑なロジックが絡んだステートを宣言するのに適しています。

useReducerは以下のように使います。こちらが用意するのはreducerinitialStateの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の活用です(クラスコンポーネント時代のshouldComponentUpdatePureComponentに相当)。これを活用してコンポーネントの余計な再レンダリングを避けることが、Reactアプリの基本的なパフォーマンス・チューニングです。

この例では、useReducerReact.memoの利用の助けになる例を示し、丁寧に解説します。

初期状態のサンプル

まず、改善前の初期状態を見てみましょう。以下のCodeSandboxで実際に動作を確かめることができます。初期状態のコードはApp1.tsxに入っています。

今回の題材はこの画像のようなものです。

2fdbb139875c810db99b763f8a9f7c33.png

4つの入力欄があり、それぞれに数値を入力することができます。下には4つの数値を合計した値が表示されます。また、入力欄の横にある「check」ボタンを押すと、そのときの数値が合計の何%かを一番下に表示します。画像は「123」の横のボタンを押したあとの状態です。

一見意味不明な例に見えますが、これは実は筆者が実際に業務で経験した例をかなり単純化したものになっています。

この記事にも初期状態のコードを一気に貼り付けます。記事を読みつつコードを見たいという方は適宜CodeSandboxをご活用ください。記事中でも部分ごとに解説していきますから、ここで全部読む必要はありません。

src/App1.tsx
import 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つの状態を持ちます。以下に示すvaluesmessageです。

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に再レンダリングが発生してしまいます。

今回のゴールは、NumberInputReact.memoを適用して無駄な再レンダリングを減らすことです。特に、ひとつの数値が変更されたらそのNumberInputだけが再レンダリングされて、他のNumberInputは再レンダリングされないという状態が理想です。

お察しの通り、最終的にはuseReducerを用いてこれを達成することになります。

React.memo導入への努力

とりあえず、まずはuseStateのまま努力してみましょう。NumberInputReact.memoを適用して効果を得るためには、他の入力値が変わってもpropsの内容が変化しないようにしなければいけません。現状ではvalueは問題ありませんが、onChangeonCheckが問題です。あの位置に関数をベタ書きということは、これらのpropsには毎回異なる関数オブジェクトが作られて渡されています。これではReact.memoは効きません。

こういうときの定石はuseCallbackです。とはいえ、今回はループでNumberInputを表示しているのでひと工夫必要です。筋のいい方法としては、NumberInputに「自分が何番目か」を表すpropsを渡すという方法があります1。これをコールバックに渡してもらうことで、onChangeonCheckは全ての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を受け取るようになりました。これが、自身が何番目かを表す数値です。onChangeonCheckの型も変更され、これらの関数にはindexがオウム返しで渡されるようになっています。先ほども説明した通り、これによりonChangeonCheckを各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が追加されているのに加え、onChangeonCheckが事前に用意されるようになりました。次に、これらを用意する部分のコードです。

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 /* 省略 */
}

onChangeonCheckuseCallbackに囲まれています。それぞれの関数の中身は、indexを引数で受け取るようになった以外は変わりません。

できることは全部やったように見えますが、残念ながらこのコードはまだ目的を達成できていませんonChangeuseCallbackにより常に同じ関数オブジェクトになっているのでOKですが、onCheckが問題です。

onCheckuseCallbackの第二引数が[values]となっています。これは、valuesが変わるたびに、すなわち何か入力が変わるたびに、onCheckが作りなおされるということを意味します。これによりNumberInputに渡されるonCheck関数が毎回別物になるため、React.memoが無意味になっています。

では、なぜuseCallbackの第二引数がvaluesを含んでいなければいけないのでしょうか。それはもちろん、onCheckvaluesに依存しているからです。つまり、onCheckが中で「入力値の合計」を求めるためにvaluesを使用しているのです。onCheckのインターフェースが(index: number) => voidである、すなわちindexのみを受け取るという関数である以上、valuesというデータについてはonCheckに内包されていなければいけません。これにより、必然的にvaluesが変わるたびにonCheckという関数は別物になります。

一方で、onChangevaluesに依存していません。これは、useStateが提供するステート更新関数が、関数によるステートの更新をサポートしているからです。上のコードではsetValues関数の引数として「現在の状態を受け取って次の状態を返す関数」を渡しています。この機能により、onChangeからvaluesへの依存を消しているのです。

となると、onCheckmessageというステートを更新するにあたって、それとは別のvaluesというステートに依存していることが問題だと分かります。これを解消するためには、2つのステートを合体させて1つのステートにする必要があります。

このような状況に適しているのがuseReducerです。ということで、AppuseReducerを用いて書き換えることで問題を解決しましょう。(一応、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)}%です`
      };
    }
  }
};

型定義を読むと、Statevaluesmessageをひとつにまとめたオブジェクトであることが分かります。アクションは"input""check"の2種類があり、それぞれ前回のコードのonChangeonCheckに相当するロジックが書かれています。

次に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になりました。従来のonChangeonCheckがひとつにまとまっていますね。それ以外は特に変わっていません。

最後に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により行われています。従来onChangeonCheckが担っていたロジックはreducerの中に押し込められましたので、ここでは何もせずにただNumberInputdispatchを渡すだけになっています。

前のコードと比べると、ここに本質的なポイントがあります。それは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つのステート(valuesmessage)を1つに合体させる」ということです。これにより、onCheckでも関数によるステート更新ができるようになりました。実を言えばuseStateでも頑張ればこれは達成できますが、このような複雑なステートを扱うにはuseReducerが適しているのでここではuseReducerを選択することになります。useReducerを使う場合はステート更新関数(dispatch)は自動的にステートに非依存になります(reducerはそもそも「現在のステートを受け取って次のステートを計算する」というものであるため)。

useReducerのすすめ

このように、useReducerを用いることで、ステート更新関数をステート非依存にすることを強制できます。実際のアプリ開発においては、アプリが複雑化するにつれて、あるステートと別のステートが関わりを持ち始めるかもしれません。もっと具体的に言えば、あるステートを更新するときに別のステートを見る必要が発生するかもしれません。そのときがuseReducer導入のサインです。ぜひリファクタリングしてuseReducerを導入しましょう。

なぜuseReducerが必要なのか、この記事を読んだ皆さんはしっかりと説明できることでしょう。ステート更新関数がステートに非依存であることは、React.memoの活用には必須だからです。

また、useReducerReact.memoの恩恵を最大限受けるためには、できるだけreducerにロジックを詰め込むことが鍵となります。そのためには、アプリの状態は何でもステートで表現することが重要です。言い換えれば、これは手続き的なロジックを書かず、状態は明示的・宣言的に扱うということです。

また、useReducerを活かすためにはそのためのコンポーネント設計も重要です。今回の例では多少天下り的でしたが、NumberInputindexを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で解決できると思った方も多いでしょう。実際、以下のようにすればonCheckvaluesに非依存にすることができます。

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に反映され、onCheckvaluesを参照するかわりにvaluesRef.currentを参照するようにしています。useRefが返すvaluesRefは常に同じオブジェクトであることが保証されていますから、onCheckvaluesRefに依存することはありません。この方法でも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を導入しましょう。


  1. ややアクロバットな別解として、useMemoを用いて各NumberInput用のコールバックを用意するというものもあります。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】Vueコンストラクタ関数をWebコンソール上で見つける方法

Vue コンストラクタ関数とは?

Vue.jswebpack などのバンドラと共に使用している方にとっては、この Vue のことです:

import Vue from 'vue';
       ^^^

<script> タグを貼り付けるだけのいわゆる「CDN版」の Vue.js を利用している場合は window.Vue で簡単に Vue コンストラクタ関数を参照できますが、バンドラを用いてビルドされている場合においても Vue コンストラクタ関数を参照できる方法を紹介します。

vue_constructor_func_01.png

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 が有効となっていることにも驚いた方が多かったようです。

この 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_constructor_func_02.png

バージョン情報の取得・ランタイム限定ビルドであるか判定する

ランタイムビルド・完全ビルドについて詳しくは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.compileundefined となっています:
vue_constructor_func_04.png

完全ビルドの場合は、 Vue.compile が定義されています:
vue_constructor_func_03.png

状態を変更したり特定のページに遷移したりする

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 を書いたりするときに便利かもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

obniz-nobleでBLEスキャンをグラフ表示してみた

obniz-nobleは手軽に使えていい感じですね!

今回は、ブラウザからESP32にobniz-nobleで接続して、周辺のBLEデバイスのRSSIをグラフ表示してみます。
完成図はこんな感じです。

image.png

ブラウザから開いたのち、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をご参照ください。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでの条件分岐 4つの方法のメモ

はじめに

Reactでの条件付きレンダリングの方法のメモです。

目次

  1. returnするJSXを分岐
  2. 変数にJSXを格納
  3. もう少し短く!!
  4. これで最後!!
  5. まとめ

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つがシンプルかなと思います。
ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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> ... ご意見、改善点などございましたら、ぜひフィードバックをください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ページ遷移先でリロードしないと非同期通信(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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む