20200101のJavaScriptに関する記事は22件です。

NoodlでWeb Speech API Speech Recognitionを使う!Noodl Javascriptノードの使い方も解説

※この記事はNoodl2.0を使用しています

NoodlでWeb Speech API Speech Recognitionを使う方法です。
音声認識を使うとなると、バックエンド側の開発が必要となり大変ですよね。
Chromeのブラウザのみ対応になりますが、Web Speech API Speech Recognitionを使うと
数行のJavascriptで簡単にマイクから拾った音声をテキストにすることができます!

参考にした記事

Web Speech API Speech Recognitionについて、詳細はこちら
Webページでブラウザの音声認識機能を使おう - Web Speech API Speech Recognition

サンプルページはコチラ
※micro:bitは抜いてます。

サンプルの使い方

1. Noodlのプロジェクトをクローンする

ここからクローンし、Noodl2.0で読み込んでください。

2. chromeをブラウザから開く

スクリーンショット 2020-01-01 22.09.56.png
右上のブラウザアイコン→URLをクリックします。

スクリーンショット 2020-01-01 22.10.12.png
URLの「192.168.xxx.xxx」を「localhost」に変更する。
※ブラウザの制限で、マイク機能はlocalhostかhttpsでないと動かないためです。サーバーにデプロイすることでも解消されます。

3. ラーメン画像をクリックし、マイクを許可

ラーメンタップで音声認識が開始します。

NoodlのJavascriptノードの解説

Noodl1.3のJavascriptノードと少し使用が変わっているようです。
基本的な使い方の説明と、NoodlでJavascriptノードで非同期通信するときのコツを記述します。

inputsとoutputs

inputs: 任意の入力ポートを作ることができる。
outputs: 任意の出力ポートを作ることができる。

書き方
inputの名前:'データ型'
利用可能な型:'number', 'string', 'boolean', 'color', 'signal', 'collection'

mySignal:function

特定のシグナルが来た場合に実行したいときに使用します。
例えば、タップしたら実行するなど。

【任意のinput名その1boolean)】:function{
//実行したい処理
},
【任意のinput名その2boolean)】:function{
//実行したい処理
}

このように、複数追加もできるようです。
Noodl1.3ではこのような処理はif文で書いていました。2.0のほうがスッキリかけそうですね。

change:function

inputの値のどれかが変更されたときに実行される。

このプロジェクトのJavascriptノードの中身

サンプルでは、ラーメンをタップしたときにmySignalにtrueのシグナルを送り、音声認識を実行させています。

define({
    // The input ports of the Javascript node, name of input and type
    inputs:{
        // ExampleInput:'number',
        // Available types are 'number', 'string', 'boolean', 'color' and 'signal',
        mySignal:'signal'
    },

    // The output ports of the Javascript node, name of output and type
    outputs:{
        // ExampleOutput:'string',
        text: "string",
        received: "boolean",
        finalTextReceived:"boolean"
    },

    // All signal inputs need their own function with the corresponding name that
    // will be run when a signal is received on the input.
    mySignal:function(inputs,outputs) {
        // ...
        outputs.received = false;
        outputs.finalTextReceived = false;

        outputs.received = false;
        SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
          const recognition = new SpeechRecognition();

          recognition.interimResults = true;

          recognition.onresult = (event) => {
            outputs.text = event.results[0][0].transcript;
            this.flagOutputDirty("text");
            this.sendSignalOnOutput("received");
            console.log(event.results[0].isFinal);
            outputs.finalTextReceived = (event.results[0].isFinal) ? true:false;
            this.flagOutputDirty("finalTextReceived");
          }

          recognition.start();
    },

    // This function will be called when any of the inputs have changed
    change:function(inputs,outputs) {
        // ...
    }
})


})


Javascriptノードで非同期通信をするときのコツ

Javascriptノード内で非同期通信をすると、
レスポンスが返ってきているのにoutputsが変更されない!という問題に遭遇します。

基本的にNoodlはinputsに変更があったときに中の関数を実行するため、
実行後にレスポンスが返ってきても、きちんとトリガーを作らない限りoutputsに反映されません。

こうした問題を解決するため、JavascriptノードにはいくつかのAPIが用意されています。

this.flagOutputDirty("変更を反映させたいoutputの名前")
inputsの変更以外をトリガーに、outputsを変更させるときに使用します。
このサンプルでは、音声認識のレスポンスが返ってきたときに、認識結果の文字列を"text"の出力に反映させています。

this.sendSignalOnOutput("任意のoutputの名前(boolean)")
素早くfalseからtrueへの往復を繰り返すブーリアンの信号を発信します。
このサンプルでは、音声認識のレスポンスが返ってきたときに信号を送信し、
音声認識中のアニメーション(ラーメンの拡縮)を止めるトリガーにしています。

まとめ

Javascript数行で音声認識ができました!すごい!
対応ブラウザがchromeのみなのが不便ですが、今後、他のブラウザにも実装されることを祈ります...!

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

【JavaScript】JavaScript、その前に〜Webサイトが表示されるまで・レンダリング

JavaScriptの勉強に必要な事前知識です。追求すると底がありませんが、最低限必要な内容をまとめました。

  • Webページが表示されるまでの流れ
  • ブラウザの役割
  • JavaScriptの役割

Webサイトが表示されるまで

アドレスバーにURLを入力し、画面上にWebページが表示されるまでの流れです。通信でどのようなデータがやり取りされているか、確認しておきます。

図解

HTTP通信.png

ブラウザ

上図のクライアントに該当するアプリケーションがブラウザです。動作の結果は「Webページを表示する」ですが、内部では様々なことが行われています。

ブラウザの構成については、意外とネット上の情報が少なかったです。
自分は下記サイトを参考にしました。

ブラウザの仕組み: 最新ウェブブラウザの内部構造
ウェブサイトが表示されるまでにブラウザはどういった仕事を行っているのか?

明確に構造が分かれているかは分かりませんが、少なくとも理解しやすくなるかと思います。

構成

ユーザーインターフェイス (UI)

アドレスバーや戻る/進むボタン、ブックマークメニューなど、ブラウザ画面のうち要求したページが表示されるメインウィンドウを除くすべての部分です。

ブラウザエンジン

ユーザーインターフェイスとレンダリングエンジン間で、情報の受け渡しをします。

レンダリングエンジン

レスポンスのボディ(本文)からリソース(HTMLファイルやCSSファイル、画像データなど)を受け取った後、構文を解析し画面にテキストと画像を描画します。

ネットワーキング

主としてHTTPによりサーバと通信し、コンテンツを取得します。

JavaScriptエンジン

JavaScriptコードの解析と実行を行います。

UIバックエンド

チェックボックスやウィンドウなどのコアウィジェット(クライアントがWebページ上で操作を行うための部品)を描画します。

データストレージ

Cookieなど、Webページで扱うデータを保存します。

HTML5でブラウザ内の軽量なデータベースであるウェブストレージ(Web Storage)が定義されました。ウェブストレージはセッションストレージ(Window.sessionStorage)ローカルストレージ(Window.localStorage)の二種類があります。

これらのストレージはAPI(Storageインターフェイス)が提供されています。

代表的なブラウザ

ブラウザ レンダリングエンジン JavaScriptエンジン
Google Chrome Blink V8
Opera Blink V8
Safari WebKit Nitro
Firefox Gecko SpiderMonkey
Microsoft Edge EdgeHTML Chakra
Internet Explorer Trident Chakra

参考リンク

レンダリング

レスポンスを受け取った後、ブラウザで画面表示されるまでの流れです。

  1. Download
  2. Perse
  3. Scripting
  4. Style
  5. Layout
  6. Paint
  7. Rasterize
  8. Composite Layers

図解

レンダリング.png

工程

ブラウザのレンダリングエンジンJavaScriptエンジンが実行します。

Download

サーバからリソースをダウンロードします。

Parse

HTMLとCSSの構文解析を行い、それぞれのオブジェクトモデルを構築します。

オブジェクトモデルは以下の段階を経て作成されます。

  1. バイト
  2. 文字
  3. トークン
  4. ノード
  5. オブジェクトモデル

レンダリングの結果、Webページを構成する要素がAPI(Application Programming Interface)として利用できるようになります。そしてAPIは、JavaScriptから利用することができます。

オブジェクトモデルはAPIの集合体です。オブジェクトモデルに変換されることで、JavaScriptコードから操作が可能になります。

HTML

HTMLを解析して作られるオブジェクトモデルがDOM(Document Object Model)です。最終的にHTMLのタグがツリー構造の集合体になりますが、これをDOMツリーと呼びます。

ドキュメントオブジェクトモデル (DOM)
DOM ツリー

CSS

CSSから変換されるオブジェクトモデルはCSSOM(CSS Object Model)です。ツリー構造はCSSOMツリーと呼ばれます。

CSS Object Model

Scripting

必要があればJavaScriptを解析し、コードを実行します。

この工程はJavaScriptエンジンが担当します。

  1. 字句解析 → トークン
  2. 構文解析 → 構文木
  3. コンパイル → 実行ファイル
  4. 実行

Style (Calculate Style)

DOMツリーとCSSOMツリーを組み合わせます。
マッチングの完成形はレンダーツリー(Render Tree)と呼ばれます。

JavaScriptによるDOM操作が行われた時などは、再度実行されます。

Layout (Reflow)

それぞれの要素の位置と大きさの計算を行い、レイアウトツリー(Layout Tree)が構築されます。

JavaScriptによる変更が行われた時などは、再度実行されます。

Paint

画面へ対する描写命令(Paint Records)を作成します。

Rasterize

ピクセル(画素)を描画したレイヤー(階層構造)を作成します。集合体はレイヤーツリー(Layer Tree)と呼ばれます。

レイヤーで分離することにより、内容変更時に再計算による負荷が減ります。

Composite Layers

レイヤーを合成し、ブラウザに表示する画面を出力します。

Google Chromeにおける表記

Google Chromeのデベロッパーツールでは、以下の表記でまとめられています。Performanceタブから確認することができます。

Chrome DevToolsを使用してサイトのパフォーマンス改善をしてみた

項目 工程
Loading Download, Perse
Scripting Scripting
Rendering Style, Layout
Painting Paint, Rasterize, Composite Layers

参考リンク

仮想DOMについて

2020年1月2日追記。
@hiro_matsuno2 さん、ご指摘ありがとうございました。

ReactVue.jsなどのフレームワークでは仮想DOM(Virtual DOM)という概念が使われています。

仮想DOMと内部処理

こちらの記事で、仮想DOMの概要が分かりやすく説明されています。

VirtualDOMの仕事ってなに?(Reactの表示速度がはやい理由)

  • 最初に仮想DOMを構築し、それを元にリアルDOMを構築する。
  • ツリー構造に変化があれば、新しく仮想DOMが構築される。
  • その後、旧仮想DOMと新仮想DOMの差分を割り出し、その差分だけリアルDOMが再構築される。

仮想DOMの構築はリアルDOMほど時間がかからないそうです。リアルDOMも一から構築し直す必要がなくなり、それゆえに早いとのこと。

参考リンク

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

フロントエンド素人大学院生が爆速でVue.jsでポートフォリオを作った話【Firebaseと連携編】

始めに

明けましておめでとうございます.(執筆時:2020/1/1)
年末年始は一緒にゆっくりする友達もいないのでやったことない技術分野にチャレンジしています.
前回までの記事は

です.

最終的に作ったもの(再掲)

実際に作ったポートフォリオはこちら
スクリーンショット 2019-12-31 6.24.22.png

github pagesで公開しています.
また,ソースコードはこちら

今回のゴール

前回までで,【表示させたいコンテンツ】を【cardコンポーネント】に表示することが出来るようになりました.
しかし,【表示させたいコンテンツ】はソースコードにハードコーディングしてしまっていて,美しくありません.
このままでは,【表示させたいコンテンツ】をちょっとだけ編集したいときに,ソースコードを編集して,masterブランチにマージして,本番環境でbuildして...ってめんどくさいです.

そこで,【表示させたいコンテンツ】はどこかから動的に取ってくるような仕組みにしようと思います.
そのような仕組みを作るのに便利なのが,FirebaseというgoogleのBaasです.
(本当はただただFirebaseを使ってみたいだけ)

Firebaseには枚挙に暇がないくらい便利なものがたくさんあるのですが,
今回はその中でも,Cloud Firestoreを使います.
Cloud FirestoreはいわゆるNoSQLデータベースというものを提供してくれます.
Cloud Firestoreに【表示させたいコンテンツ】を保存させておき,Vue.jsで取得して表示させる仕組みを作っていきましょう.

ちなみに,本記事の環境は,
- vue cli4でプロジェクトを作ってgithub pagesに公開する + travisでCI/CD
- フロントエンド素人大学院生が爆速でVue.jsでポートフォリオを作った話
- フロントエンド素人大学院生が爆速でVue.jsでポートフォリオを作った話【Cardデザイン】
で作った環境を前提としています.

参考

Cloud FireStoreとVue.jsでデーターベース通信を行う

事前準備

Firebaseにアプリを作成

Firebaseを使うにあたって,まずはFirebaseにアプリを登録する必要があります.

Firebaseへアクセスし,[使ってみる]
ボタンを押してください.
(Firebase自体を初めて使う人は,登録作業が必要になるかもしれません.)
スクリーンショット 2020-01-01 17.43.51.png

遷移した先のページで,[プロジェクトの追加]ボタンを押してください.
そして,画面の誘導にしたがってプロジェクトの作成を行ってください.

無事プロジェクトが作成できたら,次のような画面になると思います,
スクリーンショット 2020-01-01 17.50.21.png

次は,上記画面の,</>を押してWebアプリを追加していきます.
このとき,アプリのニックネームは適当に好きな名前を登録し,Firebase Hostingにはチェックを入れないでください
手順2で,何やらコードが現れますが後でも見れるので一旦無視して登録を完了してください.

すると,先ほど作ったアプリケーションがコンソールのTopページに現れるはずです.
スクリーンショット 2020-01-01 17.58.55.png
そこで,そのアプリケーションをクリックし,歯車マークをおし設定画面を開いてください.
設定画面の下の方に,Firebase SDK snippetというセクションがあり,なにやらコードのようなものが書いてあると思います.
これをソースコードのしかるべき部分に書く必要があるのですが,それは後で書くとして,今はこのFirebase SDK snippetがこの手順で見れることだけ知っておいてください.

Firestoreの作成

次に,先に【表示させたいコンテンツ】をFirestoreに登録しておきましょう.
ちなみに,Firestoreでのデータの構造は,

コレクション > ドキュメント > フィールド

になっています.

  • フィールドはキーと値のセット
  • ドキュメントは複数のフィールドを持つ
  • コレクションは複数のドキュメントを持つ

という感じです.
一応,ドキュメントの中にサブコレクションという物を入れ子にすることもできますが,今回は出てきません.

では,左側のサイドバーから,Databaseというところをクリックしてください.
スクリーンショット 2020-01-01 18.07.20.png

すると,オレンジ色のヘッダーに[データベースの作成]というボタンがあるので,そこをクリックしてください.
スクリーンショット 2020-01-01 18.09.36.png
このようなポップアップが出てきますが,下のテストモードで開始をチェックし,次へを押してください.
次のポップアップのリージョンはデフォルトのままでOKです.
しばらく待つと,このような画面になります.(一部黒抜きしてます.)
スクリーンショット 2020-01-01 18.12.00.png
この画面の,[コレクションを開始]ボタンを押してください.
コレクションIDは,プロジェクト内で一意であればなんでも良いです.
今回は,profileItemsとします.
次の画面では,コレクションを追加するためのフォームが出てきます.
ここのドキュメントIDもユニークである必要がありますが,ここは自動IDで構わないでしょう.
スクリーンショット 2020-01-01 18.45.19.png
画像のように,適当にtitle, description, commentフィールドを作ってください.
同様に数回ドキュメントを追加してみてください.
このようにドキュメントが複数あればOKです.スクリーンショット 2020-01-01 18.48.01.png

これで読み取る用のデータの作成は終わりました,

プロジェクトにfirebaseを追加.

次に,Vue.jsFirebaseを操作できるように,プロジェクトにfirebaseを追加します.
プロジェクトの直下で,

npm install firebase

をしてインストールしてください.

次に,Firebaseの設定用ファイルを作成します.

touch src/firebase.js

src/firebase.jsを作り,以下の内容を書き込んでください.
//ここにFirebase SDK snippetを入れる.はまだ無視して大丈夫です.)

// Your web app's Firebase configuration

import firebase from 'firebase/app'
import 'firebase/firestore'

var firebaseConfig = {
 //ここにFirebase SDK snippetを入れる.
}
// Initialize Firebase
firebase.initializeApp(firebaseConfig)

export default firebase

さて,次は実際に

var firebaseConfig = {
 //ここにFirebase SDK snippetを入れる.
}

の部分を書き込む必要があります.
これは,Firebaseでアプリを作成の最後の部分で見たFirebase SDK snippetを使います.
先ほどFirebase SDK snippetを見たページに移動してください.
今回は,この[構成]と書かれた方にチェックして出てくるコードを使います.

スクリーンショット 2020-01-01 19.06.42.png

このページの

var firebaseConfig = {
 apiKey: /*
いろいろな項目
*/
measurementID:xxxxxxx
}

をコピペして,そのまま

var firebaseConfig = {
 //ここにFirebase SDK snippetを入れる.
}

と置換してください.

Firestoreからデータを取得する.

とりあえず,下準備が整ったので,実際にFirestoreからデータを取得してみようと思います.
前回までで作成した,src/views/page1.vueを持ちます.

<template>
  <div class="page1 container" id="page1">
    <h1>This is Page1</h1>
    <div class="card-deck row">
      <div
        v-for="(item, key) in profileItems"
        v-bind:key="key"
        class="col-xs-12 col-md-4"
      >
        <profile-item v-bind="item" />
      </div>
    </div>
  </div>
</template>

<script>
  import ProfileItem from '../components/ProfileItem.vue'
  export default {
    components: {
      ProfileItem
    },
    data() {
      return {
        profileItems: [
          { title: 'title1', description: 'description1', comment: 'comment1' },
          { title: 'title2', description: 'description2', comment: 'comment2' },
          { title: 'title3', description: 'description3', comment: 'comment3' },
          { title: 'title4', description: 'description4', comment: 'comment4' },
          { title: 'title5', description: 'description5', comment: 'comment5' },
          { title: 'title6', description: 'description6', comment: 'comment6' }
        ]
      }
    }
  }
</script>

<style scoped>
  .page1 {
    height: 1000px;
    background-color: aqua;
  }
</style>

この,profileItemsに格納されているデータを,ソースコードに直書きするのではなく.Firestoreから取得するようにします.
まずは,<script>部で,先ほど作成したfirebase.jsを読み込みます.
ついでに,profileItemsを空にしておきましょう.

<script>
  import ProfileItem from '../components/ProfileItem.vue'
  import firebase from '../firebase.js'
  export default {
    components: {
      ProfileItem
    },
    data() {
      return {
        profileItems: []
      }
    }
  }
</script>

では,実際にデータの取得処理を書いていきますがその前に公式のドキュメントへのリンクを貼っておきます.
Cloud Firestore を使ってみる
Cloud Firestore でデータを取得する

まず,操作したいコレクションを指定してコレクション操作のためのインスタンスを作る必要があります.

firebase.firestore().collection('<コレクション名>')

今回は,profileitemsという名前でコレクションを作っているので次のように書きます.

firebase.firestore().collection('profileitems')

これを,<script>部に書いて,methodsから利用できるようにしましょう.

<script>
  import ProfileItem from '../components/ProfileItem.vue'
  import firebase from '../firebase.js'
  export default {
    components: {
      ProfileItem
    },
    data() {
      return {
        profileItems: []
      }
    },
    computed: {
      tableDatabase: () => firebase.firestore().collection('profileitems')
    }
   }
</script>

これで,methodsからthis.tableDatabaseとすることでprofileitemsのデータベースにアクセスできるようになりました.

次に,profileitemsコレクションには,複数のドキュメントがあるので,それらを全て取得する方法を見てみましょう.

コレクションのすべてのドキュメントを取得する

let citiesRef = db.collection('cities');
let allCities = citiesRef.get()
  .then(snapshot => {
    snapshot.forEach(doc => {
      console.log(doc.id, '=>', doc.data());
    });
  })
  .catch(err => {
    console.log('Error getting documents', err);
  });

このように書いてますね.
ちなみに,公式ドキュメントでは既にdbが定義されていますが,これはここでいうfirebase.firestore()だと思って大丈夫です.

どうやら,

this.tableDatabase.get()

で全てのコレクションが取得でき,それらに対してforEachを回してあげれば良さそうです.

ところで,公式のドキュメントには,snapshotとかいうのが使われていますね.これは,その関数を実行した時点でのデータが格納されているようです.
さらに.docにはそれぞれのドキュメントのデータが格納され,doc.data()でそのドキュメントに格納されているフィールドが取得できそうです.

というわけで,このようなmethodがかけそうです.

methods: {
      listenData: function() {
        return new Promise(resolve => {
          resolve(
            this.tableDatabase.get().then(snapshot => {
              snapshot.forEach(doc => {
                this.profileItems.push(doc.data())
              })
            })
          )
        })
      }
    }

これで,listenData()を実行するとそれぞれのドキュメントがprofileItemsに格納されます.
listenData()は,ページの読み込み時に一回だけ実行されて欲しいので,

created() {
      this.listenData()
}

としましょう.

従って,<script>部は,以下のようになります.

<script>
  import ProfileItem from '../components/ProfileItem.vue'
  import firebase from '../firebase.js'
  export default {
    components: {
      ProfileItem
    },
    data() {
      return {
        profileItems: []
      }
    },
    computed: {
      tableDatabase: () => firebase.firestore().collection('profileitems')
    },
    methods: {
      listenData: function() {
        return new Promise(resolve => {
          resolve(
            this.tableDatabase.get().then(snapshot => {
              snapshot.forEach(doc => {
                this.profileItems.push(doc.data())
              })
            })
          )
        })
      }
    },
    created() {
      this.listenData()
    }
  }
</script>

これで,

npm run serve

をしてhttp://localhost:8080/にアクセスすると,先ほどFirestoreに登録したデータが表示されるはずです.

これで今回の作業はほとんど完成ですが,あとは先ほどのfirebase.jsをリファクタリングします.

firebase.jsって公開されてもいいの?

最初はfirebase.jsってapiKeyとか含んでるのにパブリックなrepositoryで管理するのはまずいと思い,travisCIに環境変数として登録しておいて,デプロイ時に展開されるようにしてました.

でもよく考えるとどうせgithub pagesにアクセスするときに晒されるじゃんって思ったり.そもそも公式ではhtmlにべたがきするように指示してあったり...

どうすべきか悩んでいたら
Firebase apiKey ってさらしていいの? ほんとに?
を見つけました.

一応問題ないみたいですね.

終わりに

今回の最終的なソースコードはこちら

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

JSコードデザインOne Component One Object(OCOO)オクーのススメ!

フロントエンドプロジェクトでのJavaScriptコードデザインの課題

アプリケーション開発でフロントエンドの主役はJavaScriptのはずなのに、フロントエンドエンジニア達のコーディング認識が一番合わない部分でもあります

CSSではOOCSS、BEM、SMACSS、FLOCSS...など様々なコードデザインのアプローチがあるのにも関わらず、JavaScriptにはそういったものがないですね

なので私がよく行うJavaScriptのコードデザイン方法(OCOO)を発信してみようと思いました

OCOO(オクー)とは

  • JavaScriptのコードデザイン方法です(私が勝手に考えて名付けました)
  • One Component One Object略してOCOO(オクー)です
  • 概念を一言でいうと一つのコンポーネントは一つのオブジェクトを有しているだけで良いという思考です
  • JavaScriptフレームワークやライブラリのコードを書く時に意識してもらえるとコンポーネントのコードを追いかけやすくなります

実装のポイント

例えばですが、フォームをVueのSFC(*.vueファイル)で作る場合はこんな感じになります
動作はこちらで確認できます

app.vue
<template>
  <form @submit.prevent="sendForm">
    <div>
      <label>
        <input
          v-model="postDataAlias.name"
          type="text"
          :placeholder="placeholderAlias.name"
        />
      </label>
      <p v-if="validationAlias.name.flag">
        <span
          v-for="(message, messageIndex) in validationAlias.name.errors"
          :key="messageIndex"
        >
          {{ message }}
        </span>
      </p>
    </div>
    <div>
      <label>
        <input
          v-model="postDataAlias.age"
          type="number"
          :placeholder="placeholderAlias.age"
        />
      </label><p v-if="validationAlias.age.flag">
        <span
          v-for="(message, messageIndex) in validationAlias.age.errors"
          :key="messageIndex"
        >
          {{ message }}
        </span>
      </p>
    </div>
    <div>
      <select name="prefecture" v-model="postDataAlias.prefectureId">
        <option
          v-for="(prefecture, index) in prefectureAlias"
          :value="prefecture.id"
          :key="index"
        >
          {{ prefecture.name }}
        </option>
      </select>
    </div>
    <div>
      <button @click.prevent="sendForm">送信</button>
    </div>
    <div>
      <pre>postJSON: {{ postDataAlias }}</pre>
    </div>
  </form>
</template>

<script>
export default {
  data() {
    return {
      formObj: {}
    };
  },
  created() {
    this.formObj = this.initFormObj();
  },
  watch: {
    postDataAlias: {
      handler(formObj) {
        const formObjKeys = Object.keys(formObj);
        const checkString = /\d/;
        const checkNumber = /\d{4,}/;
        const checkAlphabet = /[a-zA-Z]/;

        formObjKeys.forEach(keyName => {
          switch (keyName) {
            case "name":
              this.validationAlias.name.flag = false;
              this.validationAlias.name.errors = [];

              if (checkString.test(formObj.name)) {
                this.validationAlias.name.flag = true;
                this.validationAlias.name.errors.push(
                  this.validationAlias.name.messageList.message_01
                );
              }

              if (checkAlphabet.test(formObj.name)) {
                this.validationAlias.name.flag = true;
                this.validationAlias.name.errors.push(
                  this.validationAlias.name.messageList.message_02
                );
              }

              break;
            case "age":
              this.validationAlias.age.flag = false;
              this.validationAlias.age.errors = [];

              if (checkNumber.test(formObj.age)) {
                this.validationAlias.age.flag = true;
                this.validationAlias.age.errors.push(
                  this.validationAlias.age.messageList.message_01
                );
              }
              break;
          }
        });
      },
      deep: true
    }
  },
  computed: {
    prefectureAlias: {
      get() {
        return this.formObj.prefecture;
      }
    },
    placeholderAlias: {
      get() {
        return this.formObj.placeholder;
      }
    },
    validationAlias: {
      get() {
        return this.formObj.validation;
      }
    },
    postDataAlias: {
      get() {
        return this.formObj.postData;
      }
    }
  },
  methods: {
    initFormObj() {
      return {
        prefecture: [
          { id: 0, name: "" },
          { id: 1, name: "北海道" },
          { id: 2, name: "青森" },
          { id: 3, name: "岩手" },
          { id: 4, name: "宮城" },
          { id: 5, name: "..." }
        ],
        placeholder: {
          name: "山田 太郎",
          age: 30
        },
        validation: {
          name: {
            flag: false,
            errors: [],
            messageList: {
              message_01: "数字を含まない文字列でご入力ください",
              message_02: "アルファベット以外でご入力ください"
            }
          },
          age: {
            flag: false,
            errors: [],
            messageList: {
              message_01: "4桁以下の数値にしてください"
            }
          }
        },
        postData: {
          name: "",
          age: "",
          prefectureId: ""
        }
      };
    },
    sendForm() {
      // POSTの処理
      alert("postJSONが送信されるデータです");
    }
  }
};
</script>

<style>
p {
  margin-top: 0;
}
p > span {
  color: red;
  font-size: 10px;
  display: block;
}
</style>

ポイント①

コンポーネント内にオブジェクトを一つ定義する(配列でもOK)

data() {
  return {
    formObj: {}
  };
}

ポイント②

オブジェクトを関数で初期化する

オブジェクトの初期化は変数にそのまま定義してしまっても良いですが、関数で初期化した方がオブジェクトの整形や拡張などを容易にできるため関数で行うのが良いと思います

initFormObj() {
  return {
    prefecture: [
      { id: 0, name: "" },
      { id: 1, name: "北海道" },
      { id: 2, name: "青森" },
      { id: 3, name: "岩手" },
      { id: 4, name: "宮城" },
      { id: 5, name: "..." }
    ],
    placeholder: {
      name: "山田 太郎",
      age: 30
    },
    validation: {
      name: {
        flag: false,
        messages: [],
        messageList: {
          error_01: "数字を含まない文字列でご入力ください",
          error_02: "アルファベット以外でご入力ください"
        }
      },
      age: {
        flag: false,
        messages: [],
        messageList: {
          error_01: "4桁以下の数値にしてください"
        }
      }
    },
    postData: {
      name: "",
      age: "",
      prefectureId: ""
    }
  };
}

ポイント③

オブジェクトのエイリアスを作る(無くても良い)

オブジェクトのパスが長くなってしまう場合は必要に応じてエイリアスを作っても良いと思います

computed: {
  prefectureAlias: {
    get() {
      return this.formObj.prefecture;
    }
  },
  placeholderAlias: {
    get() {
      return this.formObj.placeholder;
    }
  },
  validationAlias: {
    get() {
      return this.formObj.validation;
    }
  },
  postDataAlias: {
    get() {
      return this.formObj.postData;
    }
  }
}

ポイント④

テンプレート側はオブジェクトのプロパティのみ参照する

上記のサンプルコードのテンプレートではエイリアス経由でオブジェクトのプロパティを参照しています

上記の例ではwatchv-modelで各プロパティの変更を検知してよしなに画面の表示を変更しています

基本的にはオブジェクトの値が更新されれば画面の表示も変化するようにコードを書いていきます

まとめ

One Component One Object略してOCOO(オクー)いかがでしょうか?
要はオブジェクトを一つ定義してテンプレート側はそのオブジェクトしか参照しないという考えでコードを書いていきます

上記ではvueのコードで解説を行ったため、reactだとどうなるのかみたいなところはまた時間のある時に追記していきたいと思います(Angularはvueと同じ双方向型ですので大体同じようになるかと思います)

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

JavaScriptコードデザインOne Component One Object(OCOO)オクーのススメ!

フロントエンドプロジェクトでのJavaScriptコードデザインの課題

アプリケーション開発でフロントエンドの主役はJavaScriptのはずなのに、フロントエンドエンジニア達のコーディング認識が一番合わない部分でもあります

CSSではOOCSS、BEM、SMACSS、FLOCSS...など様々なコードデザインのアプローチがあるのにも関わらず、JavaScriptにはそういったものがないですね

なので私がよく行うJavaScriptのコードデザイン方法(OCOO)を発信してみようと思いました

OCOO(オクー)とは

  • JavaScriptのコードデザイン方法です(私が勝手に考えて名付けました)
  • One Component One Object略してOCOO(オクー)です
  • 概念を一言でいうと一つのコンポーネントは一つのオブジェクトを有しているだけで良いという思考です
  • JavaScriptフレームワークやライブラリのコードを書く時に意識してもらえるとコンポーネントのコードを追いかけやすくなります

実装のポイント

例えばですが、フォームをVueのSFC(*.vueファイル)で作る場合はこんな感じになります
動作はこちらで確認できます

app.vue
<template>
  <form @submit.prevent="sendForm">
    <div>
      <label>
        <input
          v-model="postDataAlias.name"
          type="text"
          :placeholder="placeholderAlias.name"
        />
      </label>
      <p v-if="validationAlias.name.flag">
        <span
          v-for="(message, messageIndex) in validationAlias.name.errors"
          :key="messageIndex"
        >
          {{ message }}
        </span>
      </p>
    </div>
    <div>
      <label>
        <input
          v-model="postDataAlias.age"
          type="number"
          :placeholder="placeholderAlias.age"
        />
      </label><p v-if="validationAlias.age.flag">
        <span
          v-for="(message, messageIndex) in validationAlias.age.errors"
          :key="messageIndex"
        >
          {{ message }}
        </span>
      </p>
    </div>
    <div>
      <select name="prefecture" v-model="postDataAlias.prefectureId">
        <option
          v-for="(prefecture, index) in prefectureAlias"
          :value="prefecture.id"
          :key="index"
        >
          {{ prefecture.name }}
        </option>
      </select>
    </div>
    <div>
      <button @click.prevent="sendForm">送信</button>
    </div>
    <div>
      <pre>postJSON: {{ postDataAlias }}</pre>
    </div>
  </form>
</template>

<script>
export default {
  data() {
    return {
      formObj: {}
    };
  },
  created() {
    this.formObj = this.initFormObj();
  },
  watch: {
    postDataAlias: {
      handler(formObj) {
        const formObjKeys = Object.keys(formObj);
        const checkString = /\d/;
        const checkNumber = /\d{4,}/;
        const checkAlphabet = /[a-zA-Z]/;

        formObjKeys.forEach(keyName => {
          switch (keyName) {
            case "name":
              this.validationAlias.name.flag = false;
              this.validationAlias.name.errors = [];

              if (checkString.test(formObj.name)) {
                this.validationAlias.name.flag = true;
                this.validationAlias.name.errors.push(
                  this.validationAlias.name.messageList.message_01
                );
              }

              if (checkAlphabet.test(formObj.name)) {
                this.validationAlias.name.flag = true;
                this.validationAlias.name.errors.push(
                  this.validationAlias.name.messageList.message_02
                );
              }

              break;
            case "age":
              this.validationAlias.age.flag = false;
              this.validationAlias.age.errors = [];

              if (checkNumber.test(formObj.age)) {
                this.validationAlias.age.flag = true;
                this.validationAlias.age.errors.push(
                  this.validationAlias.age.messageList.message_01
                );
              }
              break;
          }
        });
      },
      deep: true
    }
  },
  computed: {
    prefectureAlias: {
      get() {
        return this.formObj.prefecture;
      }
    },
    placeholderAlias: {
      get() {
        return this.formObj.placeholder;
      }
    },
    validationAlias: {
      get() {
        return this.formObj.validation;
      }
    },
    postDataAlias: {
      get() {
        return this.formObj.postData;
      }
    }
  },
  methods: {
    initFormObj() {
      return {
        prefecture: [
          { id: 0, name: "" },
          { id: 1, name: "北海道" },
          { id: 2, name: "青森" },
          { id: 3, name: "岩手" },
          { id: 4, name: "宮城" },
          { id: 5, name: "..." }
        ],
        placeholder: {
          name: "山田 太郎",
          age: 30
        },
        validation: {
          name: {
            flag: false,
            errors: [],
            messageList: {
              message_01: "数字を含まない文字列でご入力ください",
              message_02: "アルファベット以外でご入力ください"
            }
          },
          age: {
            flag: false,
            errors: [],
            messageList: {
              message_01: "4桁以下の数値にしてください"
            }
          }
        },
        postData: {
          name: "",
          age: "",
          prefectureId: ""
        }
      };
    },
    sendForm() {
      // POSTの処理
      alert("postJSONが送信されるデータです");
    }
  }
};
</script>

<style>
p {
  margin-top: 0;
}
p > span {
  color: red;
  font-size: 10px;
  display: block;
}
</style>

ポイント①

コンポーネント内にオブジェクトを一つ定義する(配列でもOK)

data() {
  return {
    formObj: {}
  };
}

ポイント②

オブジェクトを関数で初期化する

オブジェクトの初期化は変数にそのまま定義してしまっても良いですが、関数で初期化した方がオブジェクトの整形や拡張などを容易にできるため関数で行うのが良いと思います

initFormObj() {
  return {
    prefecture: [
      { id: 0, name: "" },
      { id: 1, name: "北海道" },
      { id: 2, name: "青森" },
      { id: 3, name: "岩手" },
      { id: 4, name: "宮城" },
      { id: 5, name: "..." }
    ],
    placeholder: {
      name: "山田 太郎",
      age: 30
    },
    validation: {
      name: {
        flag: false,
        messages: [],
        messageList: {
          error_01: "数字を含まない文字列でご入力ください",
          error_02: "アルファベット以外でご入力ください"
        }
      },
      age: {
        flag: false,
        messages: [],
        messageList: {
          error_01: "4桁以下の数値にしてください"
        }
      }
    },
    postData: {
      name: "",
      age: "",
      prefectureId: ""
    }
  };
}

ポイント③

オブジェクトのエイリアスを作る(無くても良い)

オブジェクトのパスが長くなってしまう場合は必要に応じてエイリアスを作っても良いと思います

computed: {
  prefectureAlias: {
    get() {
      return this.formObj.prefecture;
    }
  },
  placeholderAlias: {
    get() {
      return this.formObj.placeholder;
    }
  },
  validationAlias: {
    get() {
      return this.formObj.validation;
    }
  },
  postDataAlias: {
    get() {
      return this.formObj.postData;
    }
  }
}

ポイント④

テンプレート側はオブジェクトのプロパティのみ参照する

上記のサンプルコードのテンプレートではエイリアス経由でオブジェクトのプロパティを参照しています

上記の例ではwatchv-modelで各プロパティの変更を検知してよしなに画面の表示を変更しています

基本的にはオブジェクトの値が更新されれば画面の表示も変化するようにコードを書いていきます

まとめ

One Component One Object略してOCOO(オクー)いかがでしょうか?
要はオブジェクトを一つ定義してテンプレート側はそのオブジェクトしか参照しないという考えでコードを書いていきます

上記ではvueのコードで解説を行ったため、reactだとどうなるのかみたいなところはまた時間のある時に追記していきたいと思います(Angularはvueと同じ双方向型ですので大体同じようになるかと思います)

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

JavaScriptコードデザインOne Component One Object(OCOO)のススメ!

フロントエンドプロジェクトでのJavaScriptコードデザインの課題

アプリケーション開発でフロントエンドの主役はJavaScriptのはずなのに、フロントエンドエンジニア達のコーディング認識が一番合わない部分でもあります

CSSではOOCSS、BEM、SMACSS、FLOCSS...など様々なコードデザインのアプローチがあるのにも関わらず、JavaScriptにはそういったものがないですね

なので私がよく利用しているJavaScriptのコードデザイン方法(OCOO)を発信してみようと思いました

OCOO(オクー)とは

  • JavaScriptのコードデザイン方法です(私が勝手に考えて名付けました)
  • One Component One Object略してOCOO(オクー)です
  • 概念を一言でいうと一つのコンポーネント(又はモジュール)は一つのオブジェクトだけを参照するという規約です
  • JavaScriptフレームワークやライブラリのコードを書く時に意識してもらえるとコンポーネントのコードを追いかけやすくなります

メリット

  • 変数宣言が散らばらない
  • 誰が書いても構成が同じになりやすいため、コードを追っかけている時に迷子になりにくい
  • 厳格過ぎないため、難しそうなアーキテクチャを取り入れるよりも気軽に使える
  • チーム内に最低限のお作法が生まれる

デメリット

  • オブジェクト操作や配列操作のメソッドを利用する場面が多くなるため、慣れが必要かもしれない
  • 既存プロジェクトへ導入しようとするとリファクタが結構大変かもしれない

実装のポイント

例えばですが、フォームをVueのSFC(*.vueファイル)で作る場合はこんな感じになります
動作はこちらで確認できます(vueのdevtoolが使えます)

フォームの機能としましては、入力された値のバリデーションチェックとサーバーへ送信するデータの作成を行うコードを書きました(説明するための完成度はあるかと:innocent:

app.vue
<template>
  <form @submit.prevent="sendForm">
    <div>
      <label>
        <input
          v-model="postDataAlias.name"
          type="text"
          :placeholder="placeholderAlias.name"
        />
      </label>
      <p v-if="validationAlias.name.flag">
        <span
          v-for="(message, messageIndex) in validationAlias.name.errors"
          :key="messageIndex"
        >
          {{ message }}
        </span>
      </p>
    </div>
    <div>
      <label>
        <input
          v-model="postDataAlias.age"
          type="number"
          :placeholder="placeholderAlias.age"
        />
      </label><p v-if="validationAlias.age.flag">
        <span
          v-for="(message, messageIndex) in validationAlias.age.errors"
          :key="messageIndex"
        >
          {{ message }}
        </span>
      </p>
    </div>
    <div>
      <select name="prefecture" v-model="postDataAlias.prefectureId">
        <option
          v-for="(prefecture, index) in prefectureAlias"
          :value="prefecture.id"
          :key="index"
        >
          {{ prefecture.name }}
        </option>
      </select>
    </div>
    <div>
      <button @click.prevent="sendForm">送信</button>
    </div>
    <div>
      <pre>postJSON: {{ postDataAlias }}</pre>
    </div>
  </form>
</template>

<script>
export default {
  data() {
    return {
      formObj: {}
    };
  },
  created() {
    this.formObj = this.initFormObj();
  },
  watch: {
    postDataAlias: {
      handler(formObj) {
        const formObjKeys = Object.keys(formObj);
        const checkString = /\d/;
        const checkNumber = /\d{4,}/;
        const checkAlphabet = /[a-zA-Z]/;

        formObjKeys.forEach(keyName => {
          switch (keyName) {
            case "name":
              this.validationAlias.name.flag = false;
              this.validationAlias.name.errors = [];

              if (checkString.test(formObj.name)) {
                this.validationAlias.name.flag = true;
                this.validationAlias.name.errors.push(
                  this.validationAlias.name.messageList.message_01
                );
              }

              if (checkAlphabet.test(formObj.name)) {
                this.validationAlias.name.flag = true;
                this.validationAlias.name.errors.push(
                  this.validationAlias.name.messageList.message_02
                );
              }

              break;
            case "age":
              this.validationAlias.age.flag = false;
              this.validationAlias.age.errors = [];

              if (checkNumber.test(formObj.age)) {
                this.validationAlias.age.flag = true;
                this.validationAlias.age.errors.push(
                  this.validationAlias.age.messageList.message_01
                );
              }
              break;
          }
        });
      },
      deep: true
    }
  },
  computed: {
    prefectureAlias: {
      get() {
        return this.formObj.prefecture;
      }
    },
    placeholderAlias: {
      get() {
        return this.formObj.placeholder;
      }
    },
    validationAlias: {
      get() {
        return this.formObj.validation;
      }
    },
    postDataAlias: {
      get() {
        return this.formObj.postData;
      }
    }
  },
  methods: {
    initFormObj() {
      return {
        prefecture: [
          { id: 0, name: "" },
          { id: 1, name: "北海道" },
          { id: 2, name: "青森" },
          { id: 3, name: "岩手" },
          { id: 4, name: "宮城" },
          { id: 5, name: "..." }
        ],
        placeholder: {
          name: "山田 太郎",
          age: 30
        },
        validation: {
          name: {
            flag: false,
            errors: [],
            messageList: {
              message_01: "数字を含まない文字列でご入力ください",
              message_02: "アルファベット以外でご入力ください"
            }
          },
          age: {
            flag: false,
            errors: [],
            messageList: {
              message_01: "4桁以下の数値にしてください"
            }
          }
        },
        postData: {
          name: "",
          age: "",
          prefectureId: ""
        }
      };
    },
    sendForm() {
      // POSTの処理
      alert("postJSONが送信されるデータです");
    }
  }
};
</script>

<style>
p {
  margin-top: 0;
}
p > span {
  color: red;
  font-size: 10px;
  display: block;
}
</style>

ポイント①

コンポーネント内にオブジェクトを一つ定義する(配列でもOK)

OCOOの核心部です

data() {
  return {
    formObj: {}
  };
}

ポイント②

オブジェクトを関数で初期化する

オブジェクトの初期化は変数にそのまま定義してしまっても良いですが、関数で初期化した方がオブジェクトの整形や拡張などを容易にできるため関数で行うのが良いと思います

initFormObj() {
  return {
    prefecture: [
      { id: 0, name: "" },
      { id: 1, name: "北海道" },
      { id: 2, name: "青森" },
      { id: 3, name: "岩手" },
      { id: 4, name: "宮城" },
      { id: 5, name: "..." }
    ],
    placeholder: {
      name: "山田 太郎",
      age: 30
    },
    validation: {
      name: {
        flag: false,
        messages: [],
        messageList: {
          error_01: "数字を含まない文字列でご入力ください",
          error_02: "アルファベット以外でご入力ください"
        }
      },
      age: {
        flag: false,
        messages: [],
        messageList: {
          error_01: "4桁以下の数値にしてください"
        }
      }
    },
    postData: {
      name: "",
      age: "",
      prefectureId: ""
    }
  };
}

ポイント③

オブジェクトのエイリアスを作る(無くても良い)

オブジェクトのパスが長くなってしまう場合は必要に応じてエイリアスを作っても良いと思います

上記のテンプレートではエイリアス経由でオブジェクトのプロパティを参照しています

computed: {
  prefectureAlias: {
    get() {
      return this.formObj.prefecture;
    }
  },
  placeholderAlias: {
    get() {
      return this.formObj.placeholder;
    }
  },
  validationAlias: {
    get() {
      return this.formObj.validation;
    }
  },
  postDataAlias: {
    get() {
      return this.formObj.postData;
    }
  }
}

ポイント④

テンプレート側はオブジェクトのプロパティのみ参照する

上記の例ではwatchv-modelで各プロパティの変更を検知してよしなに画面の表示を変更しています

<div>
  <label>
    <input
      v-model="postDataAlias.name"
      type="text"
      :placeholder="placeholderAlias.name"
    />
  </label>
  <p v-if="validationAlias.name.flag">
    <span
      v-for="(message, messageIndex) in validationAlias.name.errors"
      :key="messageIndex"
    >
      {{ message }}
    </span>
  </p>
</div>

まとめ

One Component One Object略してOCOO(オクー)いかがでしょうか?
要はオブジェクトを一つ定義してテンプレート側はそのオブジェクトしか参照しないという考えでコードを書いていきます

サンプルコードはフォーム一つを一つのコンポーネントとして実装しましたが、フォームの項目単位でコンポーネントを細分化する事ができます

そうするとコンポーネントの動作がもっと分かりやすくなりますね

上記ではvueのコードで解説を行ったため、reactだとどうなるのかみたいなところはまた時間のある時に追記していきたいと思います(Angularはvueと同じ双方向型ですので大体同じようになるかと思いますreactが終わったら追記してみたいと思います)

流行るといいな〜:smiley:

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

Riot Route でルーティング時のレンダリング前に何らかの処理をさせる手っ取り早い方法

本記事は Riot.js Advent Calendar 2019 の第22日目です.

以下, 注意事項です.

  • Riot.jsriot と呼ぶ
  • @riotjs/routeroute と呼ぶ
  • 本記事の route は v5.2.0(執筆時点の最新) とする ※v5 については別途記事を書き中 :bow:

onBeforeRoute イベントハンドラの生成

route のソースコードを見ていくと, 2つ riot コンポーネントが見つかりますが, その中の route-hoc.riot を見てみますと,

router-hoc.riotの抜粋
onBeforeRoute(path, pathToRegexp) {
  if (!match(path, this.state.pathToRegexp)) {
    this.update({
      route: null
    })
  }
},

何やら嬉しい名前のメソッドが定義されていますw また, index.js の中を見てみますと,

index.js
// 省略
import Route from './components/route-hoc.riot'
import Router from './components/router-hoc.riot'
// 省略
export {
  // 〜
  Router,
  Route
}

ふむふむ?よくわからないが, 使われているのは確かですね. では上手いことこの onBeforeRoute を使ってみましょう.

差し込み処理の追加

今回は公式のデモの一つ standalone-history.html をお借りします. <router>, <route> などのタグベースではなく, JS ベースでのルーティングの実装ですね.

standalone-history.html(変更前)
<nav>
  <a href="/hello">Hello</a>
  <a href="/user">User</a>
  <a href="/user/gianluca">Username</a>
  <a href="/goodbye">goodbye</a>
</nav>
<div id="root"></div>
<script type="module">
  const { initDomListeners, setBase } = route

  const loc = window.location

  setBase(`${loc.protocol}//${loc.host}`)

  const onRoute = (url) => root.innerHTML = `${url} and params=${JSON.stringify(url.params)}`

  route.route('/hello').on.value(onRoute)
  route.route('/user').on.value(onRoute)
  route.route('/user/:username').on.value(onRoute)
  route.route('/goodbye').on.value(onRoute)

  initDomListeners()
</script>

nav にリンクが並んでいるだけの簡単な SPA で, クリックすると対応したテキストが下の div タグに表示されます.

ではこちらに onBeforeRoute メソッドを追加していきます.

standalone-history.html(変更後)
  <nav>
    <a href="/hello">Hello</a>
    <a href="/user">User</a>
    <a href="/user/gianluca">Username</a>
    <a href="/goodbye">goodbye</a>
  </nav>
  <div id="root"></div>
  <script type="module">
    const { initDomListeners, setBase } = route

    const loc = window.location

    setBase(`${loc.protocol}//${loc.host}`)

    const onRoute = (url) => root.innerHTML = `${url} and params=${JSON.stringify(url.params)}`
+   const onBeforeRoute = () => {
+     // 現在のルーティングパスを取得し表示
+     alert(route.getCurrentRoute())
+   }

+   route.route('/hello').on.value(onBeforeRoute)
    route.route('/hello').on.value(onRoute)
    route.route('/user').on.value(onRoute)
    route.route('/user/:username').on.value(onRoute)
    route.route('/goodbye').on.value(onRoute)

    initDomListeners()
  </script>

では実行してみましょう.

127_0_0_1_8080_の内容_と_Standalone_History_Demo.png

先に Username リンクをクリックしており, div タグには /user/gianluca のテキストが表示されています. ここで Hello リンクをクリックしたものが↑のキャプチャになります.

alert で表示されるテキストは /hello となっており, Hello のルーティングそのものは実行されていますが, まだテキストは書き換わっていないため, レンダリングはブロックされています.

おわりに

あくまでこれは簡易的な方法で, 実際にはこれではダメだと思っています…というのも, 先程の処理を以下のように書き換えると, 先に onRoute!! のアラートが表示されます.

抜粋
-    const onRoute = (url) => root.innerHTML = `${url} and params=${JSON.stringify(url.params)}`
+    const onRoute = (url) => {
+      root.innerHTML = `${url} and params=${JSON.stringify(url.params)}`
+      alert("onRoute!!")
+    }
   const onBeforeRoute = () => {
     // 現在のルーティングパスを取得し表示
     alert(route.getCurrentRoute())
   }

-   route.route('/hello').on.value(onBeforeRoute)
    route.route('/hello').on.value(onRoute)
+   route.route('/hello').on.value(onBeforeRoute)

これはちょっと細かい話をすると, route は内部的には rawth というライブラリを使っており((厳密には route のコア機能はほぼ全てこの子の機能)), さらに rawth は内部的には erre というライブラリに依存しています.

で, この子は .on で検知するルーティングを配列で管理しており, 順番に実行されるため, 先に onRoute!! が表示される形となります. ですので, route.route('/hello')セットする順番を先に書く必要があります.

本当は正しい使い方があると思いますが, 一旦これで動いているのでまた何か分かったら更新したいと思います.

ではでは(=゚ω゚)ノ

参考

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

HTML要素をドラッグで動かしたい

See the Pen MovableElement by Yoshiharu (@yoshiharu2580) on CodePen.

interface ElementPosition {
  top: number;
  left: number;
}

interface PointerPosition {
  x: number | null;
  y: number | null;
}

class MovableElement {
  // Elementオブジェクト
  private element: HTMLElement;
  // 要素の初期位置
  private elementInitialPosition: ElementPosition;
  // 要素の動き始めの位置
  private elementStartPosition: ElementPosition;
  // ポインタ-の動き始めの位置
  private pointerStartPosition: PointerPosition = {
    x: null,
    y: null
  };

  constructor(id: string) {
    this.element = document.getElementById(id);
    this.elementInitialPosition = this.getCurrentPosition();

    this.element.addEventListener("mousedown", (event: MouseEvent) => {
      this.elementStartPosition = this.getCurrentPosition();

      this.pointerStartPosition.x = event.clientX;
      this.pointerStartPosition.y = event.clientY;
    });

    this.element.addEventListener("mousemove", (event: MouseEvent) => {
      if (
        this.pointerStartPosition.x === null ||
        this.pointerStartPosition.y === null
      ) {
        return;
      }

      const pointerMovingDistanceX = event.clientX - this.pointerStartPosition.x;
      const pointerMovingDistanceY = event.clientY - this.pointerStartPosition.y;
      this.changeTranslate(pointerMovingDistanceX, pointerMovingDistanceY);
    });

    window.addEventListener("mouseup", () => {
      this.resetPointerStartPosition();
    });

    this.element.addEventListener("mouseleave", () => {
      this.resetPointerStartPosition();
    });
  }

  // 要素の位置を取得する
  getCurrentPosition(): ElementPosition {
    const { top, left } = this.element.getBoundingClientRect();
    return { top, left };
  }

  // 要素の位置を移動する
  changeTranslate(movingDistanceX: number, movingDistanceY: number) {
    const x =
      this.elementStartPosition.left -
      this.elementInitialPosition.left +
      movingDistanceX;
    const y =
      this.elementStartPosition.top -
      this.elementInitialPosition.top +
      movingDistanceY;

    this.element.style.transform = `translate(${x}px, ${y}px)`;
  }

  // ポインターの動き始めの位置をリセット
  resetPointerStartPosition() {
    if (
      this.pointerStartPosition.x === null ||
      this.pointerStartPosition.y === null
    ) {
      return;
    }

    this.pointerStartPosition.x = null;
    this.pointerStartPosition.y = null;
  }
}

const movableElement = new MovableElement("element");

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

HTML要素をドラッグで動かす

See the Pen MovableElement by Yoshiharu (@yoshiharu2580) on CodePen.

interface ElementPosition {
  top: number;
  left: number;
}

interface PointerPosition {
  x: number | null;
  y: number | null;
}

class MovableElement {
  // Elementオブジェクト
  private element: HTMLElement;
  // 要素の初期位置
  private elementInitialPosition: ElementPosition;
  // 要素の動き始めの位置
  private elementStartPosition: ElementPosition;
  // ポインタ-の動き始めの位置
  private pointerStartPosition: PointerPosition = {
    x: null,
    y: null
  };

  constructor(id: string) {
    this.element = document.getElementById(id);
    this.elementInitialPosition = this.getCurrentPosition();

    this.element.addEventListener("mousedown", (event: MouseEvent) => {
      this.elementStartPosition = this.getCurrentPosition();

      this.pointerStartPosition.x = event.clientX;
      this.pointerStartPosition.y = event.clientY;
    });

    this.element.addEventListener("mousemove", (event: MouseEvent) => {
      if (
        this.pointerStartPosition.x === null ||
        this.pointerStartPosition.y === null
      ) {
        return;
      }

      const pointerMovingDistanceX = event.clientX - this.pointerStartPosition.x;
      const pointerMovingDistanceY = event.clientY - this.pointerStartPosition.y;
      this.changeTranslate(pointerMovingDistanceX, pointerMovingDistanceY);
    });

    window.addEventListener("mouseup", () => {
      this.resetPointerStartPosition();
    });

    this.element.addEventListener("mouseleave", () => {
      this.resetPointerStartPosition();
    });
  }

  // 要素の位置を取得する
  getCurrentPosition(): ElementPosition {
    const { top, left } = this.element.getBoundingClientRect();
    return { top, left };
  }

  // 要素の位置を移動する
  changeTranslate(movingDistanceX: number, movingDistanceY: number) {
    const x =
      this.elementStartPosition.left -
      this.elementInitialPosition.left +
      movingDistanceX;
    const y =
      this.elementStartPosition.top -
      this.elementInitialPosition.top +
      movingDistanceY;

    this.element.style.transform = `translate(${x}px, ${y}px)`;
  }

  // ポインターの動き始めの位置をリセット
  resetPointerStartPosition() {
    if (
      this.pointerStartPosition.x === null ||
      this.pointerStartPosition.y === null
    ) {
      return;
    }

    this.pointerStartPosition.x = null;
    this.pointerStartPosition.y = null;
  }
}

new MovableElement("element");

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

JSでHTML要素をドラッグで動かす

See the Pen MovableElement by Yoshiharu (@yoshiharu2580) on CodePen.

interface ElementPosition {
  top: number;
  left: number;
}

interface PointerPosition {
  x: number | null;
  y: number | null;
}

class MovableElement {
  // Elementオブジェクト
  private element?: HTMLElement;
  // 要素の初期位置
  private elementInitialPosition?: ElementPosition;
  // 要素の動き始めの位置
  private elementStartPosition?: ElementPosition;
  // ポインタの動き始めの位置
  private pointerStartPosition: PointerPosition = {
    x: null,
    y: null
  };

  constructor(id: string) {
    this.element = document.getElementById(id);
    this.elementInitialPosition = this.getCurrentPosition();

    this.element.addEventListener("mousedown", (event: MouseEvent) => {
      this.elementStartPosition = this.getCurrentPosition();

      this.pointerStartPosition.x = event.clientX;
      this.pointerStartPosition.y = event.clientY;
    });

    this.element.addEventListener("mousemove", (event: MouseEvent) => {
      if (
        this.pointerStartPosition.x === null ||
        this.pointerStartPosition.y === null
      ) {
        return;
      }

      const pointerMovedDistanceX =
        event.clientX - this.pointerStartPosition.x;
      const pointerMovedDistanceY =
        event.clientY - this.pointerStartPosition.y;
      this.changeTranslate(pointerMovedDistanceX, pointerMovedDistanceY);
    });

    window.addEventListener("mouseup", () => {
      this.resetPointerStartPosition();
    });

    this.element.addEventListener("mouseleave", () => {
      this.resetPointerStartPosition();
    });
  }

  // 要素の位置を取得する
  getCurrentPosition(): ElementPosition {
    const { top, left } = this.element.getBoundingClientRect();
    return { top, left };
  }

  // 要素の位置を移動する
  changeTranslate(movingDistanceX: number, movingDistanceY: number) {
    const x =
      this.elementStartPosition.left -
      this.elementInitialPosition.left +
      movingDistanceX;
    const y =
      this.elementStartPosition.top -
      this.elementInitialPosition.top +
      movingDistanceY;

    this.element.style.transform = `translate(${x}px, ${y}px)`;
  }

  // ポインタの動き始めの位置をリセット
  resetPointerStartPosition() {
    if (
      this.pointerStartPosition.x === null ||
      this.pointerStartPosition.y === null
    ) {
      return;
    }

    this.pointerStartPosition.x = null;
    this.pointerStartPosition.y = null;
  }
}

new MovableElement("element");

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

JavaScriptでHTML要素をドラッグして動かす

See the Pen MovableElement by Yoshiharu (@yoshiharu2580) on CodePen.

interface ElementPosition {
  top: number;
  left: number;
}

interface PointerPosition {
  x: number | null;
  y: number | null;
}

class MovableElement {
  // Elementオブジェクト
  private element?: HTMLElement;
  // 要素の初期位置
  private elementInitialPosition?: ElementPosition;
  // 要素の動き始めの位置
  private elementStartPosition?: ElementPosition;
  // ポインタの動き始めの位置
  private pointerStartPosition: PointerPosition = {
    x: null,
    y: null
  };

  constructor(id: string) {
    this.element = document.getElementById(id);
    this.elementInitialPosition = this.getCurrentPosition();

    this.element.addEventListener("mousedown", (event: MouseEvent) => {
      this.elementStartPosition = this.getCurrentPosition();

      this.pointerStartPosition.x = event.clientX;
      this.pointerStartPosition.y = event.clientY;
    });
    this.element.addEventListener("touchstart", (event: TouchEvent) => {
      this.elementStartPosition = this.getCurrentPosition();

      this.pointerStartPosition.x = event.changedTouches[0].clientX;
      this.pointerStartPosition.y = event.changedTouches[0].clientY;
    });

    window.addEventListener("mousemove", (event: MouseEvent) => {
      if (
        this.pointerStartPosition.x == null ||
        this.pointerStartPosition.y == null
      )
        return;

      const pointerMovedDistanceX = event.clientX - this.pointerStartPosition.x;
      const pointerMovedDistanceY = event.clientY - this.pointerStartPosition.y;
      this.changeTranslate(pointerMovedDistanceX, pointerMovedDistanceY);
    });
    window.addEventListener("touchmove", (event: TouchEvent) => {
      if (
        this.pointerStartPosition.x == null ||
        this.pointerStartPosition.y == null
      )
        return;

      const pointerMovedDistanceX =
        event.changedTouches[0].clientX - this.pointerStartPosition.x;
      const pointerMovedDistanceY =
        event.changedTouches[0].clientY - this.pointerStartPosition.y;
      this.changeTranslate(pointerMovedDistanceX, pointerMovedDistanceY);
    });

    window.addEventListener("mouseup", () => {
      this.resetPointerStartPosition();
    });
    window.addEventListener("touchend", () => {
      this.resetPointerStartPosition();
    });
  }

  // 要素の位置を取得する
  getCurrentPosition(): ElementPosition {
    const { top, left } = this.element.getBoundingClientRect();
    return { top, left };
  }

  // 要素の位置を移動する
  changeTranslate(movedDistanceX: number, movedDistanceY: number) {
    const x =
      this.elementStartPosition.left -
      this.elementInitialPosition.left +
      movedDistanceX;
    const y =
      this.elementStartPosition.top -
      this.elementInitialPosition.top +
      movedDistanceY;

    this.element.style.transform = `translate(${x}px, ${y}px)`;
  }

  // 要素の動き始めの位置をリセット
  resetPointerStartPosition() {
    if (
      this.pointerStartPosition.x == null ||
      this.pointerStartPosition.y == null
    )
      return;

    this.pointerStartPosition.x = null;
    this.pointerStartPosition.y = null;
  }
}

new MovableElement('element');

ここから色々応用できそう。

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

【Nuxt.js】SEO基礎編②:超簡単解説!5分でわかるSEO

前置き

前回の続きです。
全てにおいて
https://note.com/aliz/n/nf7cfce1bb6ca
心構え
が大事になるので、ご確認を。

3-2.SEO内部対策

使いやすいサイトを作ること
ページ内の質をあげることです。
知りたい情報で検索をし、
たどり着いた先で
良質な情報が得られることが求められます!

なぜこれが重要なのかは
基礎編をご覧ください。
クローラー、インデクサ、インデックスなど

具体的な対策方法は2つ
・インデクサビリティの最適化
・クローラビリティの最適化

インデクサビリティの最適化

クローラーに適切に情報を
インデックスしてもらうために
分かりやすいサイト構造に
どのページにどの情報があるかを判別させる
具体的なポイントを下記で説明します。

大枠の構造が…
・パンくずリストの設置
・title要素の最適化
・meta要素の最適化

など
細かいpタグなどは別記事にて。

ページネーションは
サポート終了したため触れません。
複数のページ情報を
1連のまとまりのあるものと判別するために
rel=”next”とrel=”prev”を使用していました。
それを判別しなくなったということですね。
https://webmaster-ja.googleblog.com/2011/12/relnext-relprev.html

3-2-1.パンくずリストの設置?

Frame 1.png
ページ上部によくあるこれ、
クローラーが今どこにいて
どんな情報が得られるのか
判別しやすくなります?
多くても3階層くらいが良さそうです。

3-2-2.title要素の最適化

そのページの情報を簡潔に表す言葉のため非常に重要です!
・見出しタグにキーワードを入れる
 h1〜h6の中でも
 h1にはしっかりと
 検索されるキーワードを入れましょう!
 でもいくつも盛り込めないので、
 ターゲットを絞って選定?
 尚且つ、タイトルだけで内容が想定でき、
 想定通りの情報が書かれていることが◎✨
 前回の記事
 3-1-1.検索キーワードを選定
 で選定したキーワードを入れましょう。

・h1はページに1つ!
 それ以外はいくつ使用してもOKです!
 section内にh1を1つずつの場合、
 ペナルティはないけど
 原則ページに対して1つが良さそうです。

・h1〜h6の構造を守る
 h1の下にh3がきたりとかダメ

・文字数は30文字程度に
 検索表示結果で表示される字数がこれくらいだから

3-2-3.meta descriptionの最適化

ページの説明文にあたる部分です。
検索した時にタイトルの下に出るあれです。
要約文をスペニットして表示させています。
検索順位には、
あまり影響がないと言われていますが、
クリック率に影響があります。

・選定したキーワードを盛り込む
・文字数はスマホ50文字程度、PC120文字程度
ただし、
設定した文通りに
スペニットされるわけではない
というのが難しいところですね。
こちらのサイトが参考になります!
https://digital-marketing.jp/seo/what-is-a-description/#i

3-2-4.meta content="noindex"の最適化

基礎編でやった
2-2.インデックス
これをインデックスさせないことです。
これを活用して、
現在低品質なページをnoindexにし、
良質なページの割合を増やして
評価をあげることができます?
インデックスされているかどうかは
GoogleSearchConsoleで確認ができます?
https://search.google.com/search-console/welcome

noindexについては
ここが参考になります!
https://seolaboratory.jp/91446/

このアカウントでは
Nuxt.js、Vue.jsを誰でも分かるよう、
超簡単に解説しています??
これからも発信していくので、
ぜひフォローしてください♪

https://twitter.com/aLizlab

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

FizzBuzz問題のJavaScript最短コードを解説します

FizzBuzz問題とは

問題
1から100までの数をプリントするプログラムを書け。
ただし3の倍数のときは数の代わりに「Fizz」、5の倍数のときは「Buzz」とプリントし、
3と5の両方の倍数のときには「FizzBuzz」とプリントすること。

上記問題をFizzBuzz問題と言い、エンジニアの中では有名な問題らしいです。(恥ずかしながらつい最近まで知りませんでした..)
問題自体は、if文を使って一つずつ条件を書き、for文を使って1から100までループさせることで比較的簡単に解くことができます。

FizzBuzzコードゴルフ

しかし、これを可能な限り少ないコードで書こうとすると、とたんに難しくなります。
(問題の条件を満たすプログラムをできるだけ短い文字数で実現する競技をCode Golfと呼びます。)

長いこと考えた末、、

JS
for(i=1;i<101;i++){console.log(i%15?i%3?i%5?i:'Buzz':'Fizz':'FizzBuzz')}

条件演算子を駆使して、なんとか72文字の解までたどり着きました。

が、

ネットの海に潜ってJavaScriptの最短コードを探したところ、なんと62文字の最強の解が見つかりました。
それがこれ↓

JS
for(i=0;++i<101;console.log(i%5?x||i:x+'Buzz'))x=i%3?'':'Fizz'

「なんだこれ?、意味わからん..」と思い、コードの中身を調べていたところ
そのあまりに綺麗なロジックに感動してしまいました。。

以下その感動を伝えたい解説記事です。

※最短解引用元
ES2015時代のFizzBuzzに挑戦する - Qiita
FizzBuzz JavaScript solution · GitHub

コード解説

ポイントは3点。
(1)for文の{ }を省略
(2)基礎を踏まえた繰り返しの定義
(3)神の条件分岐ロジック

(1)for文の{ }を省略

通常for文を書くとき、

for () {
//処理の文
}

のように書きますが、「処理の文」が1つのときは文を囲む{ }を省略することができます。
つまり、

for ()//処理の文

のように書くことができ、
FizzBuzz最短解ではこのショートハンドを使用しています。

// 最短解
for(i=0;++i<101;console.log(i%5?x||i:x+'Buzz'))x=i%3?'':'Fizz'


// for文の{}を省略せずに書いた場合
for(i=0;++i<101;console.log(i%5?x||i:x+'Buzz')) {
  x = i%3 ? '' : 'Fizz';
}

でも正直まだ全然わかんない..:innocent:

(2)基礎を踏まえた繰り返しの定義

最短解をよく見てみると、for文の( )の中にconsole.log()が記述されていることが分かります。

FizzBuzz解のfor文条件式
for(i=0; ++i<101; console.log(i%5?x||i:x+'Buzz'))

ここでJavaScriptのfor文の定義を振り返ってみましょう。

for文の定義
for ([initialization]; [condition]; [final-expression]) statement

第1引数のinitialization変数宣言であり、たいていはカウンタ変数を初期化するために使われます。
FizzBuzz解でもi = 0;とループの初期値を定義しています。

第2引数のconditionループの各反復の前に評価される式です。
この式がtrueに評価された場合のみ、その後のstatementが実行されます。
FizzBuzz解では++i<101;という定義になっていますが、
これはi=i+1が101より小さいときに、式がtrueとなりループが実行されるということです。
スクリーンショット 2020-01-01 21.38.24.png

第3引数のfinal-expressionループの各反復の終わりに評価される式です。
FizzBuzz解ではそこにconsole.log()を記述しているため、for文の各反復における処理の文が実行された後(反復の終わり)に、テキストが表示されるようになっています。

ここまでの内容を要約すると、

  • 各反復の初めにi=i+1が実行され(iの初期値は0)、その数が101より小さい時にfor文の処理が実行される。
  • 各反復でfor文の処理が実行された後に、console.log()が実行される。

となり、
さらにコード形式でまとめると、

// for文の定義
for ('①変数定義';
     '②ループの各反復の前に評価される式';
     '③ループの各反復の終わりに評価される式')
    {'②がtrueの時に実行される処理'}

// FizzBuzz最短解にあてはめる
for (i=0;
     ++i<101; // i=i+1が101より小さい時、つまりi+1が1〜100の時にループが実行される
     console.log(i%5?x||i:x+'Buzz')) // よってconsole.log()が100回実行される
    { x = i%3 ? '' : 'Fizz'; }

ということになります。
だいぶ見えてきたけど、まだ表示されるテキスト部分が分からんぞ?:thinking:

(3)神の条件分岐ロジック

(3)-1 反復処理の内容

次にFizzBuzz最短解において、for文で反復実行される処理の内容を見ていきます。

FizzBuzz解の反復処理の内容
x = i%3 ? '' : 'Fizz';

この文はif文のショートハンドである条件演算子を使用しており、

条件演算子を使った条件分岐
'条件式'  'trueの場合の処理' : 'falseの場合の処理';

i%3trueの時はx=''falseの時はx='Fizz'となるよう変数xを定義しています。

補足1:%演算子
割り算の余りを求めることができる。i%3はiを3で割った余り。
ある整数を3で割って余りが0ならその数は3の倍数であると言える。

補足2:JavaScriptのtrue/false
0はboolean型でfalseの初期値を持つ。他にもnullundefined''(空文字)などもfalseとなる。
逆にその値がundefinednullでないオブジェクトは、条件文に通されると全てtrueに評価される。

今回の条件分岐をif文で書くと、

if文による条件分岐の書き換え
if ( i%3 ) {
  x = '';     // iが3の倍数ではないとき
} else {
  x = 'Fizz'  // iが3の倍数のとき
}

こうなり、
また表にまとめると、
スクリーンショット 2020-01-01 18.00.52.png
こうなります。

(3)-2 console.log()で表示するテキスト

いよいよ最後!
実際にconsole.log()で表示されるテキストを見てみます。

FizzBuzz解において各反復後に実行される処理
console.log(i%5?x||i:x+'Buzz')

これを半角スペースを入れて少し分かりやすく書くと、

console.log( i%5 ? x||i : x+'Buzz')

条件演算子を使った(3)-1と同様の条件分岐であることが分かり、
i%5trueの時はx||iを、falseの時はx+'Buzz'を表示するよう定義されていることが理解できます。

これを表でまとめると、
スクリーンショット 2020-01-01 18.32.56.png
こうなり、
さらに(3)-1で解説したxの値と合わせてまとめたものが以下の表になります。
スクリーンショット 2020-01-01 22.36.16.png

注目すべきは、x||iの部分。
論理演算子である||は、xtrueの場合はxの値をそのまま返し、falseの場合はiを返します。
これによって、iが3の倍数の時は'Fizz'を表示し、そうでない時は1から100の数字であるiをそのまま表示できるようになっています。

4つの場合分けパターンに合わせた、神がかった条件定義はまさに感動モノ..

まとめ

解説は以上です!
ここまでの内容を理解できていれば、FizzBuzz最短解のコードロジックも読み取れるはず!

FizzBuzz最短解
for(i=0;++i<101;console.log(i%5?x||i:x+'Buzz'))x=i%3?'':'Fizz'

改めて見ても.. これ考えた人天才だ....:sunglasses:

参考にしたサイト

for - JavaScript | MDN
条件 (三項) 演算子 - JavaScript | MDN
代入演算子 - JavaScript | MDN
論理演算子 - JavaScript | MDN
Boolean - JavaScript | MDN
JavaScript ショートコードテクニック集(ES6含む) - Qiita

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

Express \(Node.js\) の Graceful shutdown

基本的な実装の仕方と、実装した場合 / しなかった場合、で実際にどういう動作をするか〜、について書きます。

Linux, Node.js 12.13.0, での話だけをします。

Graceful shutdown ?

Express (Node.js) に限りませんが、Web サーバーを停止する際、クライアントから接続中のリクエスト (リクエスト受付してまだレスポンスしていない接続) はどうなるでしょうか?

Graceful shutdown とは一般的に以下の停止を指します。

  • 停止指示後に、新しい接続を受付しない
  • 残った処理中の接続が完了するのを待ってから、プロセスを安全に停止する

SIGNALs

そもそも Web サーバープロセスはどうやって停止するかというと、 SIGNAL を用いて停止します。

具体的には下表の通り、コマンド等によって SIGNAL を送信できます。

SIGNAL kill command Linux Terminal Kubernetes 実装依存 説明
SIGHUP kill -SIGHUP {pid} -- -- Yes プロセスが端末から切断された際のシグナル
SIGINT kill -SIGINT {pid} CTRL + C -- Yes 割り込み
SIGKILL kill -SIGKILL {pid} -- SIGTERM 送信から30秒後に送信 NO プロセスの実装に依存しないOSからの強制終了
SIGTERM kill {pid} -- 最初に送信 Yes プロセスの終了

最近では、プロセスを Docker コンテナ化して、 Kubernetes 等のプラットフォーム上で動かす事が多いと思います。

例えば Kubernetes では、プロセス (Docker コンテナ) を終了する際は、まず SIGTERM が送信され、30秒待ってもプロセスが終了しない場合、最終的に SIGKILL が送信されます。

実装例

URL /sleep と、 SIGNAL SIGTERM での Graceful shutdown を Express で実装しました。

index.js
const express = require('express');

const SLEEP_MSEC = 30 * 1000;
const app = express();

app.get('/sleep', (req, res) => {
  setTimeout(() => res.send('OK'), SLEEP_MSEC);
});

const server = app.listen(3000, () => console.log('Example app listening on port 3000!'));

process.on('SIGTERM', () => {
  server.close(() => {
    console.log('Process terminated.')
  })
});

実行します。

$ node index.js

Example app listening on port 3000!

ブラウザで以下の URL にアクセスすると、30秒待たされた後 OK と表示されます。

[未実装で] 実際に停止してみる

まずは 未実装の状態で どう動作するか検証します。

Express サーバーを起動した後、ブラウザで http://localhost:3000/sleep にアクセスします。
まだリクエストの処理中に SIGNAL を送信します。

(1) SIGHUP

シグナルを送信でプロセスが即座に停止。

kill -SIGHUP {pid}
Hangup: 1

(2) SIGINT

シグナルを送信でプロセスが即座に停止。

kill -SIGINT {pid}
(terminal 上の出力はなし)

(3) SIGKILL

シグナルを送信でプロセスが即座に停止。

kill -SIGKILL {pid}
Killed: 9

(4) SIGTERM

シグナルを送信でプロセスが即座に停止。

kill {pid}
Terminated: 15

[SIGTERM 実装で] 実際に停止してみる

実装していない SIGTERM 以外のシグナルは省略。

(5) SIGTERM

ブラウザで http://localhost:3000/sleep にアクセスする。

応答がすぐには帰ってこず、ローディング状態になります。

シグナルを送信します。

kill {pid}

プロセスは実行されたまま、無反応。

(terminal 上の出力はなし)

ブラウザから新しいタブを開いて http://localhost:3000/sleep に追加でアクセスする。

TCP 接続が拒否され、ブラウザ上に「正常に接続できませんでした」と表示される。

1度目のブラウザからのリクエストが 30 秒後に正常に応答が返り、プロセスが終了する。

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

Express (Node.js) の Graceful shutdown

基本的な実装の仕方と、実装した場合 / しなかった場合、で実際にどういう動作をするか〜、について書きます。

Linux, Node.js 12.13.0, での話だけをします。

Graceful shutdown ?

Express (Node.js) に限りませんが、Web サーバーを停止する際、クライアントから接続中のリクエスト (リクエスト受付してまだレスポンスしていない接続) はどうなるでしょうか?

Graceful shutdown とは一般的に以下の停止を指します。

  • 停止指示後に、新しい接続を受付しない
  • 残った処理中の接続が完了するのを待ってから、プロセスを安全に停止する

SIGNALs

そもそも Web サーバープロセスはどうやって停止するかというと、 SIGNAL を用いて停止します。

具体的には下表の通り、コマンド等によって SIGNAL を送信できます。

SIGNAL kill command Linux Terminal Kubernetes 実装依存 説明
SIGHUP kill -SIGHUP {pid} -- -- Yes プロセスが端末から切断された際のシグナル
SIGINT kill -SIGINT {pid} CTRL + C -- Yes 割り込み
SIGKILL kill -SIGKILL {pid} -- SIGTERM 送信から30秒後に送信 NO プロセスの実装に依存しないOSからの強制終了
SIGTERM kill {pid} -- 最初に送信 Yes プロセスの終了

最近では、プロセスを Docker コンテナ化して、 Kubernetes 等のプラットフォーム上で動かす事が多いと思います。

例えば Kubernetes では、プロセス (Docker コンテナ) を終了する際は、まず SIGTERM が送信され、30秒待ってもプロセスが終了しない場合、最終的に SIGKILL が送信されます。

Kubernetes - Pods - Termination of Pods

実装例

URL /sleep と、 SIGNAL SIGTERM での Graceful shutdown を Express で実装しました。

index.js
const express = require('express');

const SLEEP_MSEC = 30 * 1000;
const app = express();

app.get('/sleep', (req, res) => {
  setTimeout(() => res.send('OK'), SLEEP_MSEC);
});

const server = app.listen(3000, () => console.log('Example app listening on port 3000!'));

process.on('SIGTERM', () => {
  server.close(() => {
    console.log('Process terminated.')
  })
});

実行します。

$ node index.js

Example app listening on port 3000!

ブラウザで以下の URL にアクセスすると、30秒待たされた後 OK と表示されます。

[未実装で] 実際に停止してみる

まずは 未実装の状態で どう動作するか検証します。

Express サーバーを起動した後、ブラウザで http://localhost:3000/sleep にアクセスします。
まだリクエストの処理中に SIGNAL を送信します。

(1) SIGHUP

シグナルを送信でプロセスが即座に停止。

kill -SIGHUP {pid}
Hangup: 1

(2) SIGINT

シグナルを送信でプロセスが即座に停止。

kill -SIGINT {pid}
(terminal 上の出力はなし)

(3) SIGKILL

シグナルを送信でプロセスが即座に停止。

kill -SIGKILL {pid}
Killed: 9

(4) SIGTERM

シグナルを送信でプロセスが即座に停止。

kill {pid}
Terminated: 15

[SIGTERM 実装で] 実際に停止してみる

実装していない SIGTERM 以外のシグナルは省略。

(5) SIGTERM

ブラウザで http://localhost:3000/sleep にアクセスする。

応答がすぐには帰ってこず、ローディング状態になります。

シグナルを送信します。

kill {pid}

プロセスは実行されたまま、無反応。

(terminal 上の出力はなし)

ブラウザから新しいタブを開いて http://localhost:3000/sleep に追加でアクセスする。

TCP 接続が拒否され、ブラウザ上に「正常に接続できませんでした」と表示される。

1度目のブラウザからのリクエストが 30 秒後に正常に応答が返り、プロセスが終了する。

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

webpack4で複数のエントリポイントに応じたjs,cssファイルを出力する

複数のエントリポイントから一回のビルドコマンドで別々のjs, scss等のバンドルファイルを出力したい。

root
│  package.json
│  package-lock.json
│  webpack.config.js
│
├─assets
│  │  
│  ├─dist
│  │  ├─ js   
│  │  │  ├─A.js    
│  │  │  └─B.js
│  │  └─ css    
│  │     ├─A.css 
│  │     └─B.css
│  │
│  ├─js
│  │  ├─A.js 
│  │  ├─B.js
│  │  └─modules
│  └─sass
│     ├─A.scss 
│     ├─B.scss
│     └─modules
│        
└─node_modules

modules以下を使用して構成される[name].js及び、[name].scssから、
distのjs,css配下に[name].js及び[name].cssをbundleとしてそれぞれ生成するイメージです。

ページ単位でレイアウトとフロントに持たせる機能が全く異なるアプリケーションを作っていて、適用させる静的ファイルを完全に分けたいケースに直面したのでメモ。

bundleをcssファイルとして出力したい場合、webpack4より前ではextract-text-webpack-pluginを使用して実現することができましたが、4からはcssでの利用は非推奨になっております。

Since webpack v4 the extract-text-webpack-plugin should not be used for css. Use mini-css-extract-plugin instead.
[https://github.com/webpack-contrib/extract-text-webpack-plugin#usage]

そのため、mini-css-extract-pluginを使用します。

webpack.config.js
const path = require("path");
const MiniCssExtract = require("mini-css-extract-plugin");
const assets = path.join(__dirname, "./root/assets/");
const dist = assets + "dist/";
const froms = {
  js: {
    A: assets + "js/A.js",
    B: assets + "js/B.js"
  },
  scss: {
    A: assets + "sass/A.scss",
    B: assets + "sass/B.scss"
  }
};

module.exports = {
  mode: "development",
  entry: {
    A: froms.js.A,
    B: froms.js.B,
    "A.css": froms.scss.A,
    "B.css": froms.scss.B
  },
  output: {
    filename: "js/[name].js",
    path: dist
  },
  module: {
    rules: [
      {
        test: /\.scss/,
        use: [
          MiniCssExtract.loader,
          "css-loader",
          "sass-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtract({
      filename: "css/[name]"
    })
  ],
};

ただしこれだと、dist/jsの配下にA.css.js, B.css.jsという余計なファイルまで出力されてしまって煩わしいです。

webpack-fix-style-only-entriesを使用する

webpack-fix-style-only-entriesというライブラリを使用します。
npmインストールしてライブラリの読み込み

webpack.config.js
const FixStyleOnlyEntries = require("webpack-fix-style-only-entries");

pluginsに追記

webpack.config.js
plugins: [
    new FixStyleOnlyEntries(),
    new MiniCssExtract({
      filename: "css/[name]"
    })
  ],

ビルド

package.json
"scripts": {
  "build": "webpack"
}
$ npm run build

これで余計なjsのbundleファイルが生成されずに済みます。

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

Google提供のDataflowテンプレートとJavaScriptの話

tl;dr

  • GoogleはDataflowのテンプレートを提供しているよ
    • コードはGitHubで見られるよ
  • いくつかのテンプレートはJavaScriptで変換の処理を書けるよ
  • JavaScriptの処理はNashornを使っているよ

Google提供のDataflowテンプレートとは

名前の通り、Googleが提供していてるDataflowのテンプレートです。
これを使うことで、コードを(あまり)書かずに一定の処理が行えます。

提供されているテンプレートは、GCPのストレージ(GCS・Pub/Sub・Datastore・BigQuery・Spanner)間の移動を行うものが多く、例えば、

  • Pub/SubからBigQuery(ストリーミング)
  • GCSからBigQuery(バッチ・ストリーミング両方)

などがあります。

ちなみに、GCPのストレージ間移動以外のテンプレートとしては、Datastoreのデータ削除GCS上のファイルの解凍・圧縮などがあります

また、ソースコードがGithubで公開されているので、実装を調べたりすることが出来ます。

Google提供のDataflowテンプレートのJavaScriptとは

いくつかのテンプレートでは、入力(ソース)から出力(シンク)との間に、JavaScriptでの処理(UDF)を挟むことが出来ます。

例えば、GCSからBigQueryでは、入力と出力の間に、

  • 各行を引数として受け取る
  • 処理結果をJSONとして返す

JavaScript関数を経由させることが出来ます(使わないことも出来ます)。

全部のテンプレートでJavaScriptが使えるのではなく、これが出来るテンプレートは、例えば、

などがあります(実際にはもう少しあります)。

どうやって処理しているの

DataflowやApache Beamの機能としてJavaScriptを扱えるわけではなく、テンプレートはJavaで書いており、JavaScriptの部分はJava組み込みのJavaScriptランタイム(Nashorn)で処理しています。

JavaScriptでパイプライン自体を書けるわけではないので、注意してください(2019年時点では)。

細かいのが気なる人向け

例えば、GCSからBigQueryのテンプレートでは、ここらへんで、GCSからの各行をJavaScriptの処理に渡しています。

JavaScript系の処理はJavascriptTextTransformerクラスにまとめられており、

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

(2020年元旦時点で最新の)Stripeの決済をReactで使う

どこの決済サービスを利用するかは悩ましいところですが、業界標準のStripeはいずれにしてもおさえておきたい・・・ということで調査。意外と苦労したのでメモ。

前提知識

ネットに多くの情報がありますが、仕様が変化していて最新の情報を見つけるのに苦労しました。
事前に知っていればもっと楽だったことをまとめてみます。

Stripeのサービス

Stripeが提供するサービスはいろいろある。

  • PAYMENT(ま、普通の決済)
  • BILLING(月額課金)
  • CONNECT(プラットフォーマー用)

ここの記事では PAYMENT を扱います。

他にも色々ありますが、日本では使えないものもあるので注意(Issuingとか)。

PAYMENTの中でもいろいろ

1つのサービスの中でも自サイトへの埋め込み方法やAPIの種類など複数あります。

埋め込み方

  • Checkoutを利用する(Stripeが用意した決済画面を利用する(自分のサイトに埋め込む))
  • Stripe.js/Elementを利用する(パーツとして用意されたUIとJSを利用する)

スクリーンショット 2020-01-01 10.57.47.png

決済(API群)の種類

2019年の9月にSCA Readyである必要が発生し、その対応のためにpaymentIntentが登場したもよう。
日本で言うカード情報の「非通過」、「非保持」のためのPCI-DSS対応のようなものなやつ。

これが古い記事が参考にならない原因のようです。

  • charge(古い => 事前にtokenを作るタイプのやつ(カード情報の処理が先))
  • payementIntent(新しい => 事前にpaymentIntentを作る(カード情報の処理は後))

比較表が本家サイトにあります。

client側とserver側を実装する必要がある

プログラムはクライアントとサーバ側両方での実装が必要になります(めんどい)。
技術的には1つでもいい感じがしますが、paymentIntent作成リクエストに秘密鍵が必要なので、それを隠蔽するためかなという印象。

  • server側プログラムが必要なのは 秘密鍵 を隠蔽するため(技術的にはなくても決済自体はできる)

React

これは私の用途限定。

  • Reactに特化したelementとしてreact-stripe-elementsというパッケージがある
  • 本家サイトで紹介されているのはcharge方式。ただ、paymentIntetにも対応している
  • 本記事ではreact-stripe-elementsでpaymentIntentを利用する方法を紹介

ReactNativeだと現時点でtipsi-stripeとかを利用しないと行けないみたい(ExpoをEjectせずに利用できるライブラリは無いみたいです。。。)

paymentIntent方式のフロー

では、現時点で主流のpaymentIntetを利用する決済フローを見てみます。間違ってたらご指摘を。
フローでの処理は大きく2つ。

  1. 金額を投げてpaymentIntentを作成する(紐付いたclient_secret(tokenではない)が戻る)
  2. client_securetを利用してconfirmCardPayment()を実行すると、裏でカード情報が一緒にStripeサーバに送られる

という感じ。

まず、カード情報をStripeサーバに投げて、戻ってきたtokenを利用して金額等を投げる仕様とは逆なので注意。

図式化したイメージ。

スクリーンショット 2020-01-01 10.31.08.png

実装

では上記を踏まえて実装してみます。

準備

Stripeのアカウントとかなければ作って下さい。あとはテスト用の公開キーと秘密キーがあればいいです。

  • Stripeのアカウントを作る(なければ)
  • ダッシュボードで左メニュー下段の「テストデータの表示」をOnした状態で「公開可能キー」と「シークレットキー」をメモしておく。
    • テストだとpk_test_xxxx, sk_test_xxxxという形式。本番だとtestの部分がliveになる。
  • 処理した結佐は左メニューの「支払い」から確認できる

完成図

完成予定は下記のような感じ。決済OKならアラート出します。

スクリーンショット 2020-01-01 10.39.19.png

1つのクリックで上記2つの通信をします(ので分かりづらい)。

クライアント側

ではクライアント側から。流れはこの記事と同じですが、決済方式がchargeではなくpaymentIntetntになります。雛形作成にはcreate-react-appを利用します。

必要なモジュールのインストール。

create-react-app stripe-client
cd stripe-client
npm install --save react-stripe-elements bootstrap reactstrap formik yup

実装。App.jsと同じ階層にCheckoutForm.jsを作成して下記のようにします。

CheckoutForm.js
import React from 'react';
import { CardElement, injectStripe, CardNumberElement, CardExpiryElement, CardCVCElement, Elements } from 'react-stripe-elements';
import { Button, Form, FormGroup, Label, Input, FormFeedback } from 'reactstrap';
import { Formik } from 'formik'
import * as Yup from 'yup';

class CheckoutForm extends React.Component {

    handlePayment = async (values) => {

        // alert(JSON.stringify(values));

        const headers = new Headers();
        headers.set('Content-type', 'application/json');
        // headers.set('Access-Control-Allow-Origin', '*');

        //paymentIntentの作成を(ローカルサーバ経由で)リクエスト
        const createRes = await fetch('http://localhost:9000/createPaymentIntent', {
            method: 'POST',
            headers: headers,
            body: JSON.stringify({ amount: values.amount, username: values.username })
        })

        //レスポンスからclient_secretを取得
        const responseJson = await createRes.json();
        const client_secret = responseJson.client_secret;

        //client_secretを利用して(確認情報をStripeに投げて)決済を完了させる
        const confirmRes = await this.props.stripe.confirmCardPayment(client_secret, {
            payment_method: {
                // card: this.props.elements.getElement('card'),
                card: this.props.elements.getElement('cardNumber'),
                billing_details: {
                    name: values.username,
                }
            }
        });

        if (confirmRes.paymentIntent.status === "succeeded") {
            alert("決済完了");
        }
    }

    render() {
        console.log(this.props.stripe);
        return (
            <div className="col-8">
                <p>決済情報の入力</p>
                <Formik
                    initialValues={{ amount: 100, username: 'TARO YAMADA' }}
                    onSubmit={(values) => this.handlePayment(values)}
                    validationSchema={Yup.object().shape({
                        amount: Yup.number().min(1).max(1000),
                    })}
                >
                    {
                        ({ handleChange, handleSubmit, handleBlur, values, errors, touched }) => (
                            <Form onSubmit={handleSubmit}>
                                <FormGroup>
                                    <Label>金額</Label>
                                    <Input
                                        type="text"
                                        name="amount"
                                        value={values.amount}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.amount && errors.amount)}
                                    />
                                    <FormFeedback>
                                        {errors.amount}
                                    </FormFeedback>
                                </FormGroup>
                                <FormGroup>
                                    <Label>利用者名</Label>
                                    <Input
                                        type="text"
                                        name="username"
                                        value={values.username}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.username && errors.username)}
                                    />
                                    <FormFeedback>
                                        {errors.username}
                                    </FormFeedback>
                                </FormGroup>
                                {/* <CardElement
                                    className="bg-light p-3"
                                    hidePostalCode={true}
                                /> */}
                                <legend className="col-form-label">カード番号</legend>
                                <CardNumberElement
                                    ref={this.cardNumberRef}
                                    className="p-2 bg-light"
                                />
                                <legend className="col-form-label">有効期限</legend>
                                <CardExpiryElement
                                    className="p-2 bg-light"
                                />
                                <legend className="col-form-label">セキュリティーコード</legend>
                                <CardCVCElement
                                    className="p-2 bg-light"
                                />

                                <Button
                                    onClick={this.submit}
                                    className="my-3"
                                    color="primary"
                                >
                                    購入
                                </Button>
                            </Form>
                        )
                    }
                </Formik>

            </div>
        );
    }
}

export default injectStripe(CheckoutForm);

App.jsでCheckoutForm.jsを読み込みます。また、鍵の設定等も行います。

App.js
import React from 'react';
import { Elements, StripeProvider } from 'react-stripe-elements';
import CheckoutForm from './CheckoutForm';

function App() {
  return (
    <StripeProvider apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
      <div className="container">
        <h3 className="my-4">React Stripe Element Sample</h3>
        <Elements>
          <CheckoutForm />
        </Elements>
      </div>
    </StripeProvider>
  );
}

export default App;

これでクライアント側は一旦完了。ボタンを押すと404エラーが出るはずです。

サーバ側

続いてサーバ側。
まず、必要なモジュールをインストールします。

mkdir stripe-server
cd stripe-server
npm init -f
npm install express body-parser stripe

メイン実装。

index.js
const app = require("express")();
const stripe = require("stripe")("sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
const cors = require('cors');
const bodyParser = require('body-parser');

app.use(require("body-parser").text());
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.post('/createPaymentIntent', async (req, res) => {

    const result = await stripe.paymentIntents.create({
        amount: req.body.amount,
        currency: 'jpy',
        description: '●●商店決済', //option
        metadata: { username: req.body.username, tranId: '11111' } //option
    });

    console.log(result);
    res.json(result);

});

app.listen(9000, () => console.log("Listening on port 9000"));

stripe.paymentIntetns.create()が裏でStripeサーバと通信をしてIntentを作成しています。
作成が完了したらクライアント側でに結果を戻します。

動作確認

クライアント側

npm start

サーバ側

node index.js

Stripeダッシュボード

スクリーンショット 2020-01-01 11.17.56.png

その他

サーバ側をFirebase Functionsに展開してみましたが、問題なく動きました。
あと、Functionsは1回以上実行される可能性もあるので冪等性を確保するためのkeyを付与したほうがいいという話があります。

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

jsの>> 説明

アプリを作りたいと思いWEBに落ちているコードを参考にしていると、分からないコードがあったのでググったり動かしてみて分かった事を書いていきます。

結論

var x = 23
var x2 = x >> 1;
var x22 = Math.floor(x / 2);

console.log(x2 + ' , ' + x22)  //11 , 11

要するに
x >> p

Math.floor(x / 2**p)
は同じ値を返すということ

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

kintoneのAPIで別アプリからqueryで取得したJSONデータの処理方法

kintoneで別アプリからAPI経由でデータを取得して処理する

処理イメージ

新規で作るkintoneアプリに、テーブルを設け、別アプリ(マスターデータ登録アプリ)から、ステータスが「使用中」のものだけを引っ張ってきて、新規アプリのテーブルにデータを突っ込む。テーブルの行数は引っ張ってきたデータの件数に依存するが、随時増やす。

REST APIを使う

kintone関係のサイボウズのページを検索すると、REST APIを使うように書いてあるのだが、戻り値はJSONで返ってくる、と書いてある。JSONで返って来たデータを、巧いこと処理してテーブルに突っ込んで行かなくてはならないのだが、サイボウズのページには

sample.js
console.log(record);

みたいなlog出力のサンプルしかなくて、戻り値のJSONをどう処理するべきか、というのに非常に苦労したので、ここに書いておく。

検索条件の設定

まず、検索してデータを引っ張ってくるので、その検索条件を書く。一応、順番に並べる必要があるので、「ソートキー」というフィールドを作ってあって、そこに並び順を設定している。この辺の検索条件の設定の仕方はサイボウズkintone関係のページにたくさん書いてある。ちなみに件数は後でfor文を回すためにonにしてある。

sample.js
var body = {
    "app": 27, //検索しに行くアプリのアプリID
    "query": 'ステータス in ("使用中") order by ソートキー asc',
    totalCount: true
    };

REST APIの実行

検索条件を設定したら、実際にREST APIで検索を実行して、データを引っ張ってくるわけなのだが、引っ張って来る方法も、サイボウズkintone関係のページには書いてある。ただし、注意点があって、

検索する処理と検索結果をなんとかする処理を別に書かずにpromiseでひとつにして書く

という部分は注意が必要。

sample.js
// 別アプリからレコード用データを取得
// promiseで書かないと取得と処理が同時に走るので注意
kintone.api(kintone.api.url('/k/v1/records', true), 'GET', body).then(function(resp){
    return resp;
}, function(error) {
    console.log(error);
}).then(function(resp){
    // 取得したレコードの処理
    var records = JSON.stringify(resp);
    var results = JSON.parse(records);
    // データを作成
    var makeDataLineResult = makeDataLine(events, results, results['totalCount']);
});

取得したデータの処理

取得したデータの処理は別関数にして処理させている。上記で言うと、'makeDataLine()'なのだが、取得した件数を元に、基本は繰り返し処理。

sample.js
// 取得したデータを元にして処理
function makeDataLine(event, results, counts){
    for(var i=0; i<counts; i++){
        addRow(i, results);
    }
    deleteRow();
}

最初に書いたが、今回は取得してきたアプリで、取得してきたデータをテーブルに突っ込んでいるので、以下のような感じ。

sample.js
// 行追加
function addRow(i, results){
    const record = kintone.app.record.get().record;

    record.dataSheet.value.push({
        value: {
            "No":{
                value: i+1,
                type: 'SINGLE_LINE_TEXT',
            },
           "間隔":{
                value: results['records'][i]['作業間隔']['value'],
                type: 'SINGLE_LINE_TEXT',
            },

// --- (中略と解説) ---
// 引数で渡す前にparseしたresultsの配列にはrecordsという形で情報が入っているので、そこに順番を示す「i」をつけて、
// 検索に行った先の「フィールド名」と「value」をつければ、値が取れる。
// typeに定義するのは、情報を格納するテーブルのフィールド形式。

          "メモ":{
                value: '',
                type: 'MULTI_LINE_TEXT',
           }
        }
    });
    kintone.app.record.set({record: record});
}

ちなみに、データを書き込むアプリには予めテーブルが定義されていて、1行目にはデータを突っ込めないので、最後に消す。便宜上、データシートのナンバリングに「0」を定義しておく。

sample.js
// 1行目を消す関数
function deleteRow(){
    const record = kintone.app.record.get().record;
    const targetRowNo = 0;
    record.dataSheet.value.forEach(function(row, i){
        if(row.value.No.value === '0'){
            record.dataSheet.value.splice(i,1);
        } 
       kintone.app.record.set({record: record});
    });   
}

まとめると、REST APIで戻ってきたJSONをstringifyして、parseした上で、for文で回せばvalueがとれますよ、と。

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

ジェネレータを使った非同期処理

はじめに

ES6以降、JavaScriptの非同期処理は、GeneratorやPromiseを使って書きやすくはなりました。しかし、直列と並列を変更したり、混在させたり、何層にも入れ子にしたい場合があるので、より書きやすく読みやすい記述を検討します。

サンプル用非同期処理関数(sleep)の準備

関数内の実行状況がわかるサンプルを用意してテストします。実際のコードで使用する実用的なものとしては、画像の読み込み、AjaxでのAPI呼び出し、タイマーで起動させるものなどがあります。

テスト用サンプル関数の準備
const sleep = (time, callback) => {
  console.log('sleep time : ' + time + ' start');
  const completion = () => {
    console.log('sleep time : ' + time + ' callback');
    callback();
  };
  if (time === 0) { // 関数終了前にコールバックが呼ばれる場合のテスト用
    completion();
  } else {
    setTimeout(completion, time);
  }
};

原始的な非同期処理の例

サンプル
// 直列非同期処理
sleep(300, () => {
  sleep(200, () => {
    sleep(100, () => {
      console.log('completed.');
    });
  });
});

// 並列非同期処理
let n = 3;
const check = () => { if (--n === 0) console.log('completed.'); };
sleep(300, check);
sleep(200, check);
sleep(100, check);
テスト結果
// 直列非同期処理
sleep time : 300 start
sleep time : 300 callback
sleep time : 200 start
sleep time : 200 callback
sleep time : 100 start
sleep time : 100 callback
completed.

// 並列非同期処理
sleep time : 300 start
sleep time : 200 start
sleep time : 100 start
sleep time : 100 callback
sleep time : 200 callback
sleep time : 300 callback
completed.

ジェネレータを使った非同期処理

前述の非同期処理は直列と並列で構造が異なるため、ジェネレータを使って、直列と並列の非同期処理を同じ構造になるように書きます。直列か並列かは、serial/parallelで切り替えます。

(準備)現在実行している処理が完了してから、関数を実行する仕組み(postpone)の用意

基本的な動作としては、setTimeout(callback, 0);等と同じものです。
ここではジェネレータを使用する時に、再帰的呼び出し状態を回避するために使います。

postpone
const postpone = (callback) => {
  // setTimeout(callback, 0); は4msの制限があるため
  // gif 1x1
  const img = new Image();
  img.onload = callback;
  img.onerror = callback;
  img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
};

非同期処理ジェネレータ serial/parallel

serial/parallel
// 直列用
const serial = function (generator, completion) {
  function proceeder() {
    postpone(() => { g.next().done && completion && completion(); });
  }
  var g = generator(proceeder);
  proceeder();
};

// 並列用
const parallel = function (generator, completion) {
  let n = 0;
  function proceeder() {
    postpone(() => {
      if (n-- !== 0) return;
      completion && completion();
    });
  }
  var g = generator(proceeder);
  postpone(() => {
    while (!g.next().done) n++;
    proceeder();
  });
};

このserial/parallelを使って、前述の原始的非同期処理を書いたサンプルです。

サンプル
// 直列
serial(function* (cb) {
  yield sleep(300, cb);
  yield sleep(200, cb);
  yield sleep(100, cb);
}, () => {
  console.log('serial : end');
});

// 並列
parallel(function* (cb) {
  yield sleep(300, cb);
  yield sleep(200, cb);
  yield sleep(100, cb);
}, () => {
  console.log('parallel : end');
});
テスト結果
// 直列
sleep time : 300 start
sleep time : 300 callback
sleep time : 200 start
sleep time : 200 callback
sleep time : 100 start
sleep time : 100 callback
serial : end

// 並列
sleep time : 300 start
sleep time : 200 start
sleep time : 100 start
sleep time : 100 callback
sleep time : 200 callback
sleep time : 300 callback
parallel : end

serial/parallel関数の引数completionは必要がなければ記述を省略できます。

サンプル
serial(function* (cb) {
  yield sleep(200, cb);
  yield sleep(100, cb);
});

parallel(function* (cb) {
  yield sleep(200, cb);
  yield sleep(100, cb);
});

serial/parallelは混在させることができます。入れ子にする時は、completionで上位階層のコールバックを呼びます。

サンプル
serial(function* (cb) {
  yield sleep(700, cb);
  yield parallel(function* (cb) {
    yield sleep(600, cb);
    yield serial(function* (cb) {
      yield sleep(500, cb);
      yield sleep(400, cb);
    }, cb);
    yield parallel(function* (cb) {
      yield sleep(300, cb);
      yield sleep(200, cb);
    }, cb);
  }, cb);
  yield sleep(100, cb);
}, () => {
  console.log('serial + parallel: end');
});
テスト結果
sleep time : 700 start
sleep time : 700 callback
sleep time : 600 start
sleep time : 500 start
sleep time : 300 start
sleep time : 200 start
sleep time : 200 callback
sleep time : 300 callback
sleep time : 500 callback
sleep time : 400 start
sleep time : 600 callback
sleep time : 400 callback
sleep time : 100 start
sleep time : 100 callback
serial + parallel: end

ジェネレータを使った非同期処理の改良その1

前述のserial/parallelを入れ子にする場合、completionで上位階層のコールバックを呼びますが、

  • どの階層のジェネレータ関数の引数で与えられているコールバックが呼ばれているのかわかりにくい。
  • completion部分の記述を忘れた場合に正しく動かない。
  • そもそもcompletion部分でコールバックを指定するのが面倒である。

という問題があります。
そのため、completion部分でコールバックを呼ばなくて済むように改良します。具体的には、serial/parallelが外からコールバックを指定できる関数を返すことで、それを経由してコールバック関数を受け取り、自動で呼ばれるようにします。

非同期処理ジェネレータ serial/parallel 改良版その1

serial/parallel
const serial = (generator, completion) => {
  let callback = () => { callback = void 0; };
  const set_callback = cb => {
    callback || cb();
    callback = cb;
  };
  const proceed = () => {
    postpone(() => {
      const r = g.next();
      if (typeof r.value === 'function' && r.value.name === 'set_callback') r.value(proceed);
      if (r.done) {
        completion && completion();
        callback();
      }
    });
  };
  const g = generator(proceed);
  proceed();
  return set_callback;
};

const parallel = (generator, completion) => {
  let n = 0;
  let callback = () => { callback = void 0; };
  const set_callback = cb => {
    callback || cb();
    callback = cb;
  };
  const proceed = () => {
    postpone(() => {
      if (n-- !== 0) return;
      completion && completion();
      callback();
    });
  };
  const g = generator(proceed);
  postpone(() => {
    for (let r = g.next(); !r.done; r = g.next(), n++) {
      if (typeof r.value === 'function' && r.value.name === 'set_callback') r.value(proceed);
    }
    proceed();
  });
  return set_callback;
};
サンプル
serial(function* (cb) {
  yield sleep(700, cb);
  yield parallel(function* (cb) {
    yield sleep(600, cb);
    yield serial(function* (cb) {
      yield sleep(500, cb);
      yield sleep(400, cb);
    });
    yield parallel(function* (cb) {
      yield sleep(300, cb);
      yield sleep(200, cb);
    });
  });
  yield sleep(100, cb);
}, () => {
  console.log('serial + parallel: end');
});
テスト結果
sleep time : 700 start
sleep time : 700 callback
sleep time : 600 start
sleep time : 500 start
sleep time : 300 start
sleep time : 200 start
sleep time : 200 callback
sleep time : 300 callback
sleep time : 500 callback
sleep time : 400 start
sleep time : 600 callback
sleep time : 400 callback
sleep time : 100 start
sleep time : 100 callback
serial + parallel: end

ジェネレータを使った非同期処理の改良その2

さらに改良を加えて、ジェネレータ関数の引数及び、非同期処理関数のコールバック関数を省略できるようにします。コールバック関数を遅延設定、実行するために自前クラスTransmitterを用意しました。自前関数transmitはそれを使いやすくするヘルパ関数になります。

非同期処理ジェネレータ serial/parallel 最終形

serial/parallel
const Transmitter = class {
  constructor(executor) { // executor = callback => {...};
    this.value = void 0;  // 同期部分のreturnで返す値
    this.result = void 0; // 非同期部分のcallbackで返す値
    this._callback = result => {
      this.result = result;
      this._callback = void 0;
    };
    this.value = executor(result => { this._callback(result); });
  }
  connect(callback) {
    this._callback || callback(this.result);
    this._callback = callback;
    return this.value;
  }
};

// 最後の引数がコールバック関数となる関数のみ使用可能
// 仮のコールバック関数を強制設定する
const transmit = (func, ...args) => {
  return new Transmitter(callback => {
    if (func.length === 0) postpone(callback);
    if (func.length > 0) {
      const org = args[func.length - 1];
      if (typeof org === 'function') {
        args[func.length - 1] = () => { org(); callback(); };
      } else if (org === void 0 || org === null) {
        args[func.length - 1] = callback;
      } else {
        postpone(callback);
      }
    }
    return func(...args);
  });
};

const serial = (generator, completion) => {
  return new Transmitter(callback => {
    let y = void 0;
    const proceed = () => {
      postpone(() => {
        const r = g.next(y);
        if (!r.done) {
          y = (r.value instanceof Transmitter) ? r.value.connect(proceed) : r.value;
        } else {
          completion && completion();
          callback();
        }
      });
    };
    const g = generator(proceed);
    proceed();
  });
};

const parallel = (generator, completion) => {
  return new Transmitter(callback => {
    let n = 0;
    const proceed = () => {
      postpone(() => {
        if (n-- !== 0) return;
        completion && completion();
        callback();
      });
    };
    const g = generator(proceed);
    postpone(() => {
      let y = void 0;
      for (let r = g.next(y); !r.done; r = g.next(y), n++) {
        y = (r.value instanceof Transmitter) ? r.value.connect(proceed) : r.value;
      }
      proceed();
    });
  });
};
サンプル
serial(function* () {
  yield transmit(sleep, 700);
  yield parallel(function* () {
    yield transmit(sleep, 600);
    yield serial(function* () {
      yield transmit(sleep, 500);
      yield transmit(sleep, 400);
    });
    yield parallel(function* () {
      yield transmit(sleep, 300);
      yield transmit(sleep, 200);
    });
  });
  yield transmit(sleep, 100);
}, () => {
  console.log('serial + parallel: end');
});
テスト結果
sleep time : 700 start
sleep time : 700 callback
sleep time : 600 start
sleep time : 500 start
sleep time : 300 start
sleep time : 200 start
sleep time : 200 callback
sleep time : 300 callback
sleep time : 500 callback
sleep time : 400 start
sleep time : 600 callback
sleep time : 400 callback
sleep time : 100 start
sleep time : 100 callback
serial + parallel: end

対象となる非同期処理関数で、Transmitterを使用したり、transmitでラップしておけば、transmitの記述をする必要も無くなります。

サンプル
const t1Sleep = (time) => { return new Transmitter(callback => { sleep(time, callback); }); };
const t2Sleep = (...args) => { return transmit(sleep, ...args); };

serial(function* () {
  yield t1Sleep(400);
  yield t2Sleep(300);
  yield parallel(function* () {
    yield t1Sleep(200);
    yield t2Sleep(100);
  });
});
テスト結果
sleep time : 400 start
sleep time : 400 callback
sleep time : 300 start
sleep time : 300 callback
sleep time : 200 start
sleep time : 100 start
sleep time : 100 callback
sleep time : 200 callback

transmitを使用せずに明示的にコールバック呼ぶ形式(改良版その1の書き方)も混在できます。

サンプル
serial(function* (cb) {
  yield t1Sleep(600);
  yield t2Sleep(500);
  yield sleep(400, cb);
  yield parallel(function* (cb) {
    yield t1Sleep(300);
    yield t2Sleep(200);
    yield sleep(100, cb);
  });
});
テスト結果
sleep time : 600 start
sleep time : 600 callback
sleep time : 500 start
sleep time : 500 callback
sleep time : 400 start
sleep time : 400 callback
sleep time : 300 start
sleep time : 200 start
sleep time : 100 start
sleep time : 100 callback
sleep time : 200 callback
sleep time : 300 callback

以上です。

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

jQueryめもめも

めもめも

jQueryの書き方を頻繁に忘れるのでメモするぜ
追記するかも

基本

JavaScriptは、HTMLの読み込みが終わった後に実行!!!

jquery(document).ready(function(){
    //処理//
});

//↓↓↓↓↓省略形↓↓↓↓↓//

$(function(){
    //処理//
});

「$」は関数の名前。jquery()と機能は同じ。他のライブラリと合わせて使うと衝突するかもしれないので後者を使った方が賢明。

基本構文

$(セレクター).イベント(function() {
    $(セレクター).命令
});

セレクター:操作する要素を指定する
命令:操作する内容
イベント:操作するタイミングを決める

コールバック

メソッドの引数に関数を渡すこと

$('#target').click(function(){
   //処理//
}

メソッドチェーン

要素の指定が一度で済み、JavaScriptの実行速度も早くなる。

//2つに分けていたコードを
$("#target").css("color", "red");
$("#target").addClass("additional");
//「.」でつなげて1つに
$("#target").css("color", "red").addClass("additional");

お世話になったサイト

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