20200121のJavaScriptに関する記事は21件です。

JavaScriptの関数のかきかた!

はじめに

やっと初投稿。1ヶ月前くらいにアカウントを作ってみたものの書くこともなく放置状態だった(正確には最初にこれから頑張る!的な投稿をしたが「それ技術系の記事じゃねーだろ」と運営に言われ非公開になっております)。今の状況としては、Web系エンジニアに必要なスキルの修得を目指してとりあえずProgateの各コースを進めているところ。その中で自分が慣れ親しんだCやJava(長くやってるだけでスキルが高いわけではない)とJavaScriptでは関数の定義の仕方が結構違う感じがしたのでメモ的な感じで投稿してみる。初投稿だしいいよねこんな感じで。

Cの関数

例えば、2つの整数を引数にもち、和を戻り値として返すadd関数を宣言してみる。

int add(int x,int y){
 return x + y;
}

JavaScriptの関数

対して、JavaSciprtでこのadd関数を書くとこうなる。

const add = (x,y) => {
 return x + y;
};

まとめ

こうして並べてみるとやはりかなり違うことがわかる。引数のところに型名いらないし、関数を定数(変数)に代入してたり。言語が違うんだから書き方が違うのは当たり前なんだけど、CとJavaってここらへんの書き方はわりと似てたからまた一つ新しいことが学べていい気分。Webエンジニアへの道のりは果てしないけど頑張ろう・・・!
これからもこんな感じでゆる~く投稿していこうと思うのでどうぞよしなに。

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

Vue.js の基本的な機能を使ったサンプルを書く

概要

  • Mustache 構文、条件付きレンダリング、メソッド、算出プロパティ、フォーム入力バインディング、リストレンダリング、コンポーネントの機能を使ったサンプルコードを示す
  • 環境: Vue.js 2.6.11

Mustache 構文で Hello World

  • 画面に Hello, world! と表示するサンプルコード
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello, world!</title>
</head>
<body>

<div id="app">
  <!-- Mustache 構文で message プロパティを表示 -->
  <p>{{ message }}</p>
</div>

<!-- デバッグに便利な Vue.js の開発バージョンを使う -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<script>
// Vue インスタンスを生成
new Vue({

  // Vue インスタンスが管理する DOM 要素
  el: '#app',

  // プロパティ
  data: {
    message: 'Hello, world!'
  }
})
</script>

</body>
</html>

テンプレート構文 — Vue.js

データバインディングのもっとも基本的な形は、”Mustache” 構文(二重中括弧)を利用したテキスト展開です:

条件付きレンダリング、メソッド、算出プロパティ

  • 「カウントアップ」ボタンを押すとカウント数が1増える
  • カウント数を表示する
  • 3の倍数のときはカウント数ではなく「あほー」と表示する
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Counter</title>
</head>
<body>

<div id="myCounterDiv">

  <!-- DOM イベントをトリガーに設定 -->
  <button v-on:click="myCountUp">カウントアップ</button>

  <!-- データバインディング -->
  <!-- v-bind で title 属性にセット -->
  <!-- Mustache 構文で要素内に表示 -->
  <p v-bind:title="myMessage">カウンター: {{ myMessage }}</p>

  <!-- 条件付き描画 (Conditional Rendering) -->
  <p v-if="myCounter % 6 == 0">6の倍数ですね</p>
  <p v-else-if="myCounter % 3 == 0">3の倍数ですね</p>
  <p v-else-if="myCounter % 2 == 0">2の倍数ですね</p>
  <p v-else>2の倍数でも3の倍数でもないですね</p>
</div>

<!-- サイズと速度が最適化された Vue.js の本番バージョンを使う -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

<script>
// Vue インスタンスを生成
var vm = new Vue({

  // Vue インスタンスが管理する DOM 要素
  el: '#myCounterDiv',

  // プロパティ
  data: {
    myCounter: 0
  },

  // created フック
  // インスタンスが作成された後に同期的に呼ばれる
  created: function() {
    this.myCounter = 1
  },

  // メソッド
  methods: {
    myCountUp: function (event) {
      this.myCounter++
    }
  },

  // 算出プロパティ (computed properties)
  computed: {
    myMessage: function () {
      if (this.myCounter % 3 == 0) {
        return "あほー" // 3の倍数のときに返す値
      } else {
        return this.myCounter
      }
    }
  }
})

</script>
</body>
</html>

イベントハンドリング — Vue.js

v-on ディレクティブを使うことで、DOM イベントの購読、イベント発火時の JavaScript の実行が可能になります。

API — Vue.js

v-bind

1つ以上の属性またはコンポーネントのプロパティと式を動的に束縛します。

条件付きレンダリング — Vue.js

v-if ディレクティブは、ブロックを条件に応じて描画したい場合に使用されます。ブロックは、ディレクティブの式が真を返す場合のみ描画されます。

Vue インスタンス — Vue.js

Vue インスタンスが作成されると、自身の data オブジェクトの全てのプロパティをリアクティブシステムに追加します。これらのプロパティの値を変更すると、ビューが”反応”し、新しい値に一致するように更新します。

Vue インスタンス — Vue.js

各 Vue インスタンスは、生成時に一連の初期化を行います。例えば、データの監視のセットアップやテンプレートのコンパイル、DOM へのインスタンスのマウント、データが変化したときの DOM の更新などがあります。その初期化の過程で、特定の段階でユーザー自身のコードを追加する、いくつかの ライフサイクルフック(lifecycle hooks) と呼ばれる関数を実行します。

例えば、created フックはインスタンスが生成された後にコードを実行したいときに使われます。

算出プロパティとウォッチャ — Vue.js

算出プロパティの代わりに、同じような関数をメソッドとして定義することも可能です。最終的には、2つのアプローチは完全に同じ結果になります。しかしながら、算出プロパティはリアクティブな依存関係にもとづきキャッシュされるという違いがあります。

フォーム入力バインディング、リストレンダリング

  • フォームに入力したテキストをリストに追加
  • リストを表示
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>List</title>
</head>
<body>

<div id="myListDiv">

  <!-- フォーム入力バインディング -->
  <input v-model="myItem" placeholder="アイテム名">

  <!-- DOM イベントをトリガーに設定 -->
  <button v-on:click="myAddItem">{{ myItem }} を追加</button>

  <!-- 配列を表示 -->
  <ul>
    <li v-for="item in myItemList">
      {{ item.name }}
    </li>
  </ul>

</div>

<!-- Vue.js のバージョン 2.6.11 を使う -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>

<script>
var vm = new Vue({

  // Vue インスタンスが管理する DOM 要素
  el: '#myListDiv',

  // データ
  data: {
    myItem: '',
    myItemList: [
      { name: 'やくそう' },
      { name: 'どくけーしー' }
    ]
  },

  // メソッド
  methods: {
    myAddItem: function() {
      if (this.myItem !== '') {
        this.myItemList.push({name: this.myItem})
        this.myItem = ''
      }
    }
  },
})

</script>
</body>
</html>

フォーム入力バインディング — Vue.js

form の input 要素 や textarea 要素、 select 要素に双方向 (two-way) データバインディングを作成するには、v-model ディレクティブを使用することができます。

リストレンダリング — Vue.js

配列に基づいて、アイテムのリストを描画するために、v-for ディレクティブを使用することができます。v-for ディレクティブは item in items の形式で特別な構文を要求し、items はソースデータの配列で、item は配列要素がその上で反復されているエイリアスです:

コンポーネント

  • それぞれ別のカウント値を持ったカウンター
  • ボタンを押すとカウント数が1増える
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Counters by Components</title>
</head>
<body>

<div id="components-demo">
  <!-- それぞれが別のインスタンスになるため、それぞれ別の count プロパティを保持する -->
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

<!-- デバッグに便利な Vue.js の開発バージョンを使う -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<script>

// button-counter という新しいコンポーネントを定義
// コンポーネントは再利用可能な Vue インスタンス
Vue.component('button-counter', {

  // コンポーネントの data オプションは関数でなければならない
  data: function () {
    return {
      count: 0
    }
  },

  // Vue インスタンスに対して使用するテンプレート文字列
  // クリックするとカウントアップ count++
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

new Vue({
  // Vue インスタンスが管理する DOM 要素
  el: '#components-demo',
})

</script>
</body>
</html>

コンポーネントの基本 — Vue.js

コンポーネントは再利用可能な Vue インスタンスなので、data、computed、watch、methods、ライフサイクルフックなどの new Vue と同じオプションを受け入れます。唯一の例外は el のようなルート固有のオプションです。

コンポーネントのプロパティ

  • 表示情報のデータをコンポーネントに渡す
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Blog Posts by Components</title>
</head>
<body>

<div id="components-demo">
  <!-- 定義した blog-post コンポーネントを表示 -->
  <!-- v-bind:key で仮想 DOM 処理ヒント用にユニークなキーである myPost.id を指定 -->
  <!-- blog-post の post 属性に値 myPost を渡す -->
  <blog-post
    v-for="myPost in myPostList"
    v-bind:key="myPost.id"
    v-bind:post="myPost"
  ></blog-post>
</div>

<!-- デバッグに便利な Vue.js の開発バージョンを使う -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<script>

// blog-post という新しいコンポーネントを定義
// コンポーネントは再利用可能な Vue インスタンス
Vue.component('blog-post', {

  // データを受け取るためのプロパティ
  props: ['post'],

  // HTML 描画用テンプレート
  // Mustache 構文でタイトルを出力
  // v-html で HTML をそのまま出力
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <div v-html="post.content"></div>
    </div>
  `
})

new Vue({
  el: '#components-demo',
  data: {
    myPostList: [
      { id: 1, title: 'たいとる1', content: '<p>なかみ1</p>' },
      { id: 2, title: 'たいとる2', content: '<p>なかみ2</p>' },
      { id: 3, title: '>_<;', content: '<p>こうですか!? わかりません><</p>' }
    ]
  }
})

</script>
</body>
</html>

コンポーネントの基本 — Vue.js

プロパティはコンポーネントに登録できるカスタム属性です。値がプロパティ属性に渡されると、そのコンポーネントインスタンスのプロパティになります。

参考資料

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

ページ遷移で、真っ白な画面が表示された

事象

Reactで作ったWebページをスマホで確認していたところ、とあるページ遷移で、真っ白な画面が表示された。

コード

fetch.js
var controller = new AbortController();

fetch('https://hogehoge.com/send', {
    method: 'POST',
    signal: controller.signal,  // controllerが持つsignalをfetchに渡す
}).then((data) => {
    // 応答が得られた場合の処理
}).catch((error) => {
    controller.abort();  // fetchキャンセル
});

原因

「AbortController」をサポートしていないバージョンのChromeで読み込んだため。
この時のChromeのバージョンは「55」。
Chromeでの「AbortController」サポートは「66」以降になる。
https://developer.mozilla.org/en-US/docs/Web/API/AbortController

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

Blocklyに基づきビジュアルプログラミングの入門級の実例(三、VUE環境でBlocklyのコードを非同期的に処理するサンプル)

本章では、VUEのシンプルなサンプルで、BlocklyのBlockから生成コードを非同期的に実行する方法を説明しております。
Blocklyのコードをステップ毎に実行する時は、Interpreterライブラリをよく使っているんですけど、Interpreterライブラリを使いたくない場合は、JavaScriptのeval関数でBlocklyのコードを実行することも可能です。
evalは一般的に同期でソースコードを実行されているんで、非同期的に実行したい場合はどうしたらよいかについてのことを、以下のサンプルで説明して見ましょう。

事前準備

Blockly

公式サイト:https://developers.google.com/blockly/
Web版の資材:https://developers.google.com/blockly/guides/get-started/web#get_the_code

VUE

https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.22/vue.min.js

サンプル

testblock.js

このファイルで、画面表示用のブロックを作成します
BlocklyのDevelopToolから生成することができます。

Blockly.Blocks['block_asyncplay'] = {
  init: function() {
    this.appendStatementInput("MUSIC")
        .setCheck(null)
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField("play");
    this.setInputsInline(true);
    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour(120);
 this.setTooltip("");
 this.setHelpUrl("");
  }
};
Blockly.JavaScript['block_asyncplay'] = function(block) {
  var statements_music = Blockly.JavaScript.statementToCode(block, 'MUSIC');
  var code = "async function asyncplay() {\n" + statements_music + "\n}\n";
  return code;
};

Blockly.Blocks['block_play'] = {
  init: function() {
    this.appendDummyInput()
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField("play");
    this.setInputsInline(true);
    this.setPreviousStatement(true, "String");
    this.setNextStatement(true, "String");
    this.setColour(120);
    this.setTooltip("");
    this.setHelpUrl("");
  }
};
Blockly.JavaScript['block_play'] = function(block) {
    var code = "await play(1000);\n";
    return code;
};

test01.html

画面にブロックを表示するためのHTMLファイルです。

<script src='https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.22/vue.min.js'></script><!-- 2019-01-25 https://cdnjs.com/libraries/vue -->

    <script src="../blockly-master/blockly_compressed.js"></script>
    <script src="../blockly-master/blocks_compressed.js"></script>
    <script src="../blockly-master/javascript_compressed.js"></script>
    <script src="../blockly-master/msg/js/en.js"></script>
    <script src="./testblock.js"></script>
<body>

<div id="vue_example"></div>

<script>
var vue_example = new Vue({
  el: '#vue_example',

  template: `<div>
        <div width="600px" height="50px">
            <button v-on:click="test()">test</button>
        </div>
        <div width="600px" height="600px">
              <div id="blocklyDiv" style="height: 100%; width: 100%;"></div>
              <xml id="toolbox" ref="toolbox" style="display: none">
                <block type="block_asyncplay"></block>
                <block type="block_play"></block>
              </xml>
              <xml id="workbox" ref="workbox">
                <block type="block_asyncplay" id="id_block_asyncplay" x="10" y="30">
                    <statement name="MUSIC">
                        <block type="block_play" id="id_block_play_01">
                            <next>
                                <block type="block_play" id="id_block_play_02">
                                    <next>
                                        <block type="block_play" id="id_block_play_03"></block>
                                    </next>
                                </block>
                            </next>
                        </block>
                    </statement>
                </block>
              </xml>
        </div>
    </div>`,

  data: {
    message: 'Hello Vue.js!',
  },
  mounted() {
      var workbox = this.$refs["workbox"];
      var options = {
        toolbox: toolbox,
        collapse: true,
        comments: true,
        disable: true,
        maxBlocks: Infinity,
        trashcan: true,
        horizontalLayout: false,
        toolboxPosition: 'start',
        css: true,
        rtl: false,
        scrollbars: true,
        sounds: true,
        oneBasedIndex: true,
        grid: {
          spacing: 20,
          length: 1,
          colour: '#888',
          snap: true
        }
      }

      /* Inject your workspace */
      this.workspace = Blockly.inject('blocklyDiv', options)
      //Workspaceに書かれたBlocksを表示
      Blockly.Xml.domToWorkspace(workbox, this.workspace);

  },
  methods: {
    test: function () {

      var dom = Blockly.Xml.workspaceToDom(this.workspace);
      console.log( Blockly.Xml.domToText(dom));

      var code = Blockly.JavaScript.workspaceToCode(this.workspace);
      console.log( code );

      eval(code);
      asyncplay();

        async function play(waittime) {
            return new Promise((resolve, reject) => {
                console.log("play waittime:" + waittime);
                setTimeout(resolve, waittime, 'time signature');
            });

        }
    },

  },
})
</script>
</body>
</html>

実行結果

実行結果.png

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

【Nuxt.js】 middleware is 何?

ミドルウェアとは

  • ミドルウェアを使用すると、ページがレンダリングされる前(SSR処理などが行われる前)に設定された関数を実行することができる
  • 認証許可が必要なページやログイン後のリダイレクトパスを保持するために使用される。
  • 関数はmiddleware/ディレクトリに入れる
    • middleware/auth.jsauthミドルウェアになる
  • ミドルウェアは第一引数にcontextを取る
    • contextの内容についてはこちらを参照
  • ユニバーサルモードの場合は、サーバーサイドで一度だけ呼び出される。 (Nuxtアプリケーションへの最初のリクエスト時、またはページの再読み込み時)クライアントサイドでは、他のルートへ移動した時に呼び出される。
  • SPAモードの場合、クライアントサイドでの最初のリクエスト時と他のルートへ移動した時に呼び出される。

middlewareを使用した処理の例

storeのアカウント情報が無い場合、ログイン画面にリダイレクトされる処理を書いてみる。

middleware/redirect.js
export default function ({ redirect, store }) {
  const user = store.state.user
  if(!user) {
    redirect('/login')
  }
}

これをnuxt.config.jsで読み込む。

nuxt.config.js
export default {
  router {
    middleware: 'redirect'
  }
}

このようにすると全てのルート変更時にredirect.jsが読み込まれるようになる。

また、特定のページ(またはレイアウト)にのみ特定の関数を設定することもできる。

index.vue
<script>
import { fetchUid } from '@/middleware/uid.js'
export default {
  middleware: [ 'auth', fetchUid ]
}
</script>

1ファイルに1つの処理の場合は、'auth'で関数を実行することができる。
一方、1つのファイルに複数の関数がある場合は、ファイルをimportすることで特定の関数を実行することができる。

middlewareの実行順序について

  • middleware自体の実行順序は、このようになっている。

    • pluginsmiddlewarefetchasyncData
  • middlewareの関数の呼び出し方の違いによる実行順序はこのようになる。

    • nuxt.config.jslayoutpage

Login画面では実行したくない問題

アカウント情報を確認してリダイレクトさせる処理をmiddleware内に書いた場合、Login画面ではまだアカウント情報がないので、関数を実行したくない場合がある。
その場合は、middleware内のcontextrouteがあるので、そこから関数を実行したくないページを弾いて処理を実行させるようにする。

middleware/auth.js
export default function ({ redirect, store, route }) {
  const user = store.state.user
  if(!user && route.path !== '/login') {
    redirect('/login')
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

async(Promise)のthen()内の関数の引数の省略

備忘録として書き記す。

nuxt.jsのコードを追っていたところ下記の部分で引っかかる。

.nuxt/client.js
// Create and mount App
createApp().then(mountApp).catch(errorHandler)

コードの内容から察するにmountAppにはcreateAppの結果が引数として渡されているように見えるので実際に確かめてみた。

main.js
const first = async () => {
  return 'first'
}

const second = (arg) => {
  console.log('second')
  console.log(arg)
}

// then内の関数に自動的に引数が渡される
first().then(second)
// second
// first

// 省略しないパターン
first().then((f) => second(f))

どなたかこのような振る舞いの名称や公式で記載されているページをご存知であれば、コメントで教えていただけると幸いです。

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

【Nuxt.js】pagination実践編:$router.pushで簡単実装!

前置き

pagination.gif
ページ数に応じて
urlと表示が変わるpaginationです?
前回やった導入編と全く別物です!
こっちの方が簡単なので別パターンとして紹介?

記事タイトルが紛らわしいので、
まとめて名称変えるかもしれません。
こちらの続きはまた別記事にて…!
https://note.com/aliz/n/n8bb439a426a8

firebaseを月曜日に公開予定でしたが、
Cloud Firestoreがバグり。。。
それが落ち着いてからにします☁️
今後は火・木に投稿していく予定です!

構成

・pagination部分をコンポーネント化
・使用するページからpropsでdataを渡す?
・ページ数をurlに表示させる?($router.push)
・全7ページで、ページ数に応じて表示を変更

Step1: コンポーネントでpropsを用意

【構成】
使うコンテンツによって
最大ページ数などが変わるためpropsを使用
・query: ページネーションを使うコンテンツ
・length: ページの長さ
・now: 今いるページ

Pagination.vue
<script>
export default {
 props: {
   query: {
     type: String,
     required: true,
   },
   length: {
     type: Number,
     required: true,
   },
   now: {
     type: Number,
     required: true,
   },
 },
}
</script>

Step2: コンポーネントで戻る・進むボタンを追加

【式】三項演算を使用
式1 ? 式2 : 式3
式1がtrueなら式2、falseなら式3

Pagination.vue
<template>
 <div>
   <button
     class="btn btn-prev"
     @click="$router.push(`?${query}=${now - 1 || 1}`)"
   >
     戻る
   </button>
   <button
     class="btn btn-next"
     @click="$router.push(`?${query}=${now + 1 <= length ? now + 1 : length}`)"
   >
     進む
   </button>
 </div>
</template>

<script>
export default {
 props: {
   query: {
     type: String,
     required: true,
   },
   length: {
     type: Number,
     required: true,
   },
   now: {
     type: Number,
     required: true,
   },
 },
}
</script>

【解説】
◾️戻る

・|| または

該当コンテンツページ内queryの
今いるページnowから1戻る、または1にする

◾️進む
該当コンテンツページ内の
今いるページに1を足して
・全体ページ数と同じ
・またはそれより小さい場合
今いるページから1進む
そうでなければ全体ページ数にする

つまり全体ページを7で、
現在いるページが7なら
7 + 1 <= 7 
falseになるので7のまま
それ以上進むことはないですね?

あれ??
【戻る】すごくシンプルに見えるのに…

Pagination.vue
@click="$router.push(`?${query}=${now - 1 || 1}`)"

【進む】は何か長い。

Pagination.vue
@click="$router.push(`?${query}=${now + 1 <= length ? now + 1 : length}`)"

これではダメなの????

Pagination.vue
@click="$router.push(`?${query}=${now + 1 || length}`)"

最大ページ数を越えてどんどん進みます笑
lengthの値はマイナスにはできません。
そのため制限をかけなくても
勝手に1で止まってくれるのですが…!
プラスは制限をかけないと止まりません?‍♀️?

Step3: コンポーネントでページ数を表示

【構成】
ページ数の表示部分を作りましょう!
・5ページまではページ数分のみ表示
・6ページ以上は…(三点リーダー)で中間を省略

【CSS】
毎度のことですが省きます。
・…はcssでdotクラスでborderを使用
・現在ページがをクラスバインディングで
 background-color, colorを変更?

【if, if, if…】
ifで沢山分岐しています笑
どこで並列になってるか分かりにくいですね?
コンパクトにして全体構造を把握しましょう。

【Pagination.vue】
主にインラインのコメントで解説!
コードでも並列部分を絵文字で区別しています。
?と?が並列で使われている部分です。
それ以外の絵文字は if の目印です!

Pagination.vue
<template>
 <div
   // ページ数が1より大きい、2ページ以上の時のみページネーションを表示
   v-if="length > 1"
   class="list-item list-item-nav"
 >
   <button
     class="btn btn-prev"
     @click="$router.push(`?${query}=${now - 1 || 1}`)"
   >
     戻る
   </button>
   <ul class="list">
     // 1ページ目はどんな時でも固定表示のためif不要
     <li
       // クラスバインディング、{ class名: 式 }でtrueの時にクラスがつく
       :class="{ now: now === 1 }"
       class="item item-link"
       // 1ページを押すとurlが~/1になる
       @click="$router.push(`?${query}=1`)"
     >
       <span class="text">
         1
       </span>
     </li>
     // ?ここから分岐、最大ページ数が2より大きい3〜
     <template v-if="length > 2">
       // ?3以上5以下(=最大ページ数3,4,5の時)
            5ページまでの場合は、最大ページ数に応じて該当ページ数を表示
       <template v-if="length <= 5">
         <li
           :class="{ now: now === 2 }"
           class="item item-link"
           @click="$router.push(`?${query}=2`)"
         >
           <span class="text">
             2
           </span>
         </li>
         // ?最大ページ数が3, 4, 5かつ3より大きい4, 5の時
         <template v-if="length > 3">
           <li
             :class="{ now: now === 3 }"
             class="item item-link"
             @click="$router.push(`?${query}=3`)"
           >
             <span class="text">
               3
             </span>
           </li>
           // ?最大ページ数が3, 4, 5かつ3より大きい4, 5かつ4より大きい5の時
           <template v-if="length > 4">
             <li
               :class="{ now: now === 4 }"
               class="item item-link"
               @click="$router.push(`?${query}=4`)"
             >
               <span class="text">
                 4
               </span>
             </li>
           </template>
         </template>
       </template>
       // ?でなければ(=最大ページが5より大きい6〜)
       <template v-else>
      // ?最大ページ6〜かつ現在いるページが4より少ない(=1, 2, 3の時)
         <template v-if="now < 4">
           <li
             :class="{ now: now === 2 }"
             class="item item-link"
             @click="$router.push(`?${query}=2`)"
           >
             <span class="text">
               2
             </span>
           </li>
           <li
             :class="{ now: now === 3 }"
             class="item item-link"
             @click="$router.push(`?${query}=3`)"
           >
             <span class="text">
               3
             </span>
           </li>
           <li
             // ?現在いるページが4より少ないかつ、3ページ目にいる時
             v-if="now === 3"
             class="item item-link"
             @click="$router.push(`?${query}=4`)"
           >
             <span class="text">
               4
             </span>
           </li>
           <li class="item item-dots">
             <div class="dot" />
             <div class="dot" />
             <div class="dot" />
           </li>
         </template>
         // ?最大ページ6〜かつ現在いるページが1, 2, 3でなく4で〜
         現在いるページに2を出しても最大ページ数と同じか少なければ
         (4ページ目にいるなら4 + 2、最大ページ7の方が大きいためfalse)
              (6ページ目にいるなら6 + 2、最大ページ7より大きいためtrue)
         <template v-else-if="length <= now + 2">
           <li class="item item-dots">
             <div class="dot" />
             <div class="dot" />
             <div class="dot" />
           </li>
           // ?最大ページ数から2を引いた数字が現在いるページだったら
         最大ページ数から3を引いたページ数を表示させる
         (5ページ目にいるなら7-2 =5でtrue、7-3 =4が表示される)
           <li
             v-if="now === length - 2"
             class="item item-link"
             @click="$router.push(`?${query}=${length - 3}`)"
           >
             <span class="text">
               {{ length - 3 }}
             </span>
           </li>
           <li
             :class="{ now: now === length - 2 }"
             class="item item-link"
             @click="$router.push(`?${query}=${length - 2}`)"
           >
             <span class="text">
               {{ length - 2 }}
             </span>
           </li>
           <li
             :class="{ now: now === length - 1 }"
             class="item item-link"
             @click="$router.push(`?${query}=${length - 1}`)"
           >
             <span class="text">
               {{ length - 1 }}
             </span>
           </li>
         </template>
         // ?最大ページ6〜かつ、今までのパターンに該当しない
         (上の?のfalse、現在4ページの場合)
         <template v-else>
           <li class="item item-dots">
             <div class="dot" />
             <div class="dot" />
             <div class="dot" />
           </li>
           <li
             class="item item-link"
             @click="$router.push(`?${query}=${now - 1}`)"
           >
             <span class="text">
               {{ now - 1 }}
             </span>
           </li>
           <li class="item item-link now">
             <span class="text">
               {{ now }}
             </span>
           </li>
           <li
             class="item item-link"
             @click="$router.push(`?${query}=${now + 1}`)"
           >
             <span class="text">
               {{ now + 1 }}
             </span>
           </li>
           <li class="item item-dots">
             <div class="dot" />
             <div class="dot" />
             <div class="dot" />
           </li>
         </template>
       </template>
     </template>
     <li
       :class="{ now: now === length }"
       class="item item-link"
       @click="$router.push(`?${query}=${length}`)"
     >
       <span class="text">
         {{ length }}
       </span>
     </li>
   </ul>
   <button
     class="btn btn-next"
     @click="$router.push(`?${query}=${now + 1 <= length ? now + 1 : length}`)"
   >
     進む
   </button>
 </div>
</template>

<script>
export default {
 props: {
   query: {
     type: String,
     required: true,
   },
   length: {
     type: Number,
     required: true,
   },
   now: {
     type: Number,
     required: true,
   },
 },
}
</script>

これで完成です?

【最大ページ2は?】
if は最大ページ3以上で分岐。
2はどうなっているかというと…
ul 内の構造を黄色い枠で分けています?
・1固定表示
・3ページ以上で分岐
・最大ページ固定表示

2が最大の場合は
最大ページを表示させてるわけです?
Frame 2.png

Step4: コンテンツページでpropsにdataを渡す

【sample.vue】

Pagination.vue
<template>
<div class="page">
  <Pagination
     query="members"
     :length="7"
     :now="Number($route.query.members) || 1"
     class="nav"
   />
</div>
</template>

<script>
import Pagination from '~/components/Pagination.vue'

export default {
 components: {
   Pagination,
 },
 data() {
   return {
     members: [
       {
         name: aLiz
       },
     ],
   }
 },
}
</script>

<style lang="scss" scoped>
</style>

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

https://twitter.com/aLizlab

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

最近の要素・ノード操作型メソッド事情

はじめに

JavaScript もどんどん進化し、ライブラリ等を使わずにネイティブな記述だけでもいろんな処理が書けるようになってきています。
実際に使うかどうかはおいておき、今こんなのがあるよというやつです。

※以下、jQuery を使わない書き方を「ネイティブ」と表現しています

jQuery っぽいメソッド

もう最近では jQuery を使わないことの方が多い気がします。
なんか気づいたら jQuery で使っていたメソッドのようなものがネイティブで書けるようになってます。

.closest()

.closest() といえば、直近の祖先の要素を取得するやつです。
ネイティブでは Element.closest() として実装されています。

HTML
<div class="target">
  <div class="target">
    <div>
      <div id="grandchild"></div>
    </div>
  </div>
</div>
jQuery
var target = $('#grandchild').closest('.target');
target.prop({ id: 'grandparent' });
JavaScript
const target = document.getElementById('grandchild').closest('.target');
target.id = 'grandparent';

これは両方とも以下になります。

HTML
<div class="target">
  <div class="target" id="grandparent">
    <div>
      <div id="grandchild"></div>
    </div>
  </div>
</div>

.prepend().append()

.prepend().append() といえば、前者が指定した要素の最後に要素を挿入、後者が指定した要素の最初に要素を挿入するやつです。
ネイティブでそれぞれ ParentNode.prepend()ParentNode.append() というメソッドとして実装されています。

Node.appendChild()ParentNode.append() の違いは返り値です。
返り値については、前者は挿入される要素・ノードなのに対して、後者は undefined です。

HTML
<div id="target">
  <p>ABCDEFG</p>
</div>
jQuery
const target = $('#target');

// .prepend()
const span = $('<span>');
target.prepend(span);

// .append()
const div = $('<div>');
target.append(div);
JavaScript
const target = document.getElementById('target');

// .prepend()
const span = document.createElement('span');
target.prepend(span);

// .append()
const div = document.createElement('div');
target.append(div);

これは両方とも以下になります。

HTML
<div id="target">
  <span></span>
  <p>ABCDEFG</p>
  <div></div>
</div>

引数で複数指定が可能

.prepend().append() は引数を増やすことで複数の要素・ノードを挿入できます。

HTML
<div id="target">
  <p>ABCDEFG</p>
</div>
JavaScript
const target = document.getElementById('target');

const span01 = document.createElement('span');
const div01 = document.createElement('div');

target.prepend(span01, div01, '12345');

const span02 = document.createElement('span');
const div02 = document.createElement('div');

target.append(span02, div02, '67890');

結果は

HTML
<div id="target">
  <span></span>
  <div></div>
  12345
  <p>ABCDEFG</p>
  <span></span>
  <div></div>
  67890
</div>

ちょっと気をつけたいのが .prepend() ですね。引数で複数指定した場合は、引数の順番のまま挿入されます。

テキストの場合の挙動の違い

一見すると同じように見えますが、テキストの場合は jQuery とネイティブで違いが出てきます。
.append() で例を見ます。

HTML
<div id="test">
  <p>ABCDEFG</p>
</div>
JavaScript
const list = document.getElementById('test');

// jQuery
$(list).append('<p>jQuery</p>');

// native
list.append('<p>native</p>');

結果は……

HTML
<div id="test">
  <p>ABCDEFG</p>
  <p>jQuery</p>
  &lt;p&gt;native&lt;/p&gt;
</div>

jQuery ではHTML文字列として扱われ、ネイティブではテキストノードとして扱われます。

ネイティブでHTML文字列として挿入したい場合は element.insertAdjacentHTML で代用できます。

JavaScript
const list = document.getElementById('test');
list.insertAdjacentHTML('beforeend', '<p>native</p>');

仕様

ParentNode.append() - Web API | MDN
ParentNode.prepend() - Web API | MDN

.before().after()

.before().after() といえば、前者が指定した要素の前に要素を挿入、後者が指定した要素の後ろに要素を挿入するやつです。
ネイティブでそれぞれ ChildNode.before()ChildNode.after() というメソッドとして実装されています。

HTML
<p id="target">ABCDEFG</p>
jQuery
const target = $('#target');

// .before()
const span = $('<span>');
target.before(span);

// .after()
const div = $('<div>');
target.after(div);
JavaScript
const target = document.getElementById('target');

// .before()
const span = document.createElement('span');
target.before(span);

// .after()
const div = document.createElement('div');
target.after(div);

これは両方とも以下になります。

HTML
<span></span>
<p id="target">ABCDEFG</p>
<div></div>

引数で複数指定が可能

.before().after() は引数を増やすことで複数の要素・ノードを挿入できます。

HTML
<p id="target">ABCDEFG</p>
JavaScript
const target = document.getElementById('target');

const span01 = document.createElement('span');
const div01 = document.createElement('div');

target.before(span01, div01, '12345');

const span02 = document.createElement('span');
const div02 = document.createElement('div');

target.after(span02, div02, '67890');

結果は

HTML
<span></span>
<div></div>
12345
<p id="target">ABCDEFG</p>
<span></span>
<div></div>
67890

テキストの場合の挙動の違い

.prepend().append() の時と同じように、テキストの場合は jQuery とネイティブで違いが出てきます。

HTML
<p id="target">ABCDEFG</p>
JavaScript
const target = document.getElementById('target');

// jQuery
$(target).after('<p>jQuery</p>');

// native
target.after('<p>native</p>');

結果は……

HTML
<p id="target">ABCDEFG</p>
<p>jQuery</p>
&lt;p&gt;native&lt;/p&gt;

jQuery ではHTML文字列として扱われ、ネイティブではテキストノードとして扱われます。

ネイティブでHTML文字列として挿入したい場合は element.insertAdjacentHTML で代用できます。

JavaScript
const target = document.getElementById('target');
target.insertAdjacentHTML('afterend', '<p>native</p>');

仕様

ChildNode.before() - Web API | MDN
ChildNode.after() - Web API | MDN

.remove()

.remove() といえば、DOMツリーから要素を取り除く(削除ではない)メソッドです。
ネイティブでは ChildNode.remove() として実装されています。

Node.removeChild() との違いは、指定した要素が removeChild を実行する要素内に存在している必要があります。
要するに、取り除く要素の親要素から実行してあげる必要があります。自分自身を取り除くメソッドではありません。
ChildNode.remove() は、自分自身を取り除くメソッドです。

HTML
<p id="target">ABCDEFG</p>
<p>HIJKLMN</p>
jQuery
$('#target').remove();
JavaScript
document.getElementById('target').remove();

これは両方とも以下になります。

HTML
<p>HIJKLMN</p>

仕様

ChildNode.remove() - Web APIs | MDN

.replaceWith()

.replaceWith() は要素を置換するメソッドで、シンプルな使い方では jQuery とネイティブでは動作的にほぼ同じです。
ChildNode.replaceWith() は jQuery のように引数に関数を使えません。

HTML
<p id="target">ABCDEFG</p>
jQuery
const div = $('<div>');
$('#target').replaceWith(div);
JavaScript
const div = document.createElement('div');
document.getElementById('target').replaceWith(div);

これは両方とも以下になります。

HTML
<div></div>

テキストの場合の挙動の違い

ここまでくればおわかりかと思いますが、他のメソッド同様に挙動が異なります。

HTML
<p id="target01">ABCDEFG</p>
<p id="target02">HIJKLMN</p>
JavaScript
// jQuery
$('#target01').replaceWith('<p>jQuery</p>');

// native
document.getElementById('target02').replaceWith('<p>native</p>');

結果は……

HTML
<p>jQuery</p>
&lt;p&gt;native&lt;/p&gt;

jQuery ではHTML文字列として扱われ、ネイティブではテキストノードとして扱われます。

ネイティブでHTML文字列として挿入したい場合は Element.outerHTML で代用しようと思えばいけます。

JavaScript
const target02 = document.getElementById('target02');
target02.outerHTML = '<p>native</p>';

仕様

ChildNode.replaceWith() - Web APIs | MDN

思ったこと

相変わらずDOM系のメソッドは似たようなものがたくさんあったりしてよくわからんですね……

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

文章読み上げアプリをJavaScriptで作成してみました

はじめに

最近文章読み上げアプリをJavaScriptで作成したので作成するうえで苦労した点などをまとめてみます。
アプリはHerokuにアップしてあるので、リンクへ飛んでいただければ試していただけます。
読みあげ君へのリンクはこちら
どうして"読みあげ君”を作ろうと思ったかというと、”初音ミク”に似たソフトで誰の声でも当てられたら面白いな、と感じたことがキッカケです。

読みあげ君の画像↓

読み上げ君画像.png

苦労した点について

1.テキストエリアへ打ち込まれた文字と音声を対応させる方法
2.音声を連続で再生する方法

1.テキストエリアへ打ち込まれた文字と音声を対応させる方法

この解決策はとても単純で、連想配列を使用しました。

var voiceDictionary={1:'あ',2:'い',............71:'ぽ'};

といった風に1文字ずつキー値と対応させる方法です。
詳しいコードはリンク先で見ていただければ、と思いますが例えば、"あほか"と打ち込まれたとすると
あ→1、ほ→30、か→6といった風に連想配列を用いて数値へと変換します。
あとは、voiceDictionaryの音表通りに声のmp3ファイルを個別に配置していくことで、文字と音声の対応が出来ました。

2.音声を連続で再生する方法

最初は、for文で(打ち込まれた文字数).lengthの回数を連続で鳴らそうかと思ったのですが、何度やってもfor文ではうまくいかなかったので

(オーディオインスタンスの変数名).addEventListener('ended',function(){
//(オーディオインスタンス).play();が終了した後動作させたいコードを入れる
}

という構文を追加することで解決できました。
この構文の前に、

(オーディオインスタンス).play();

というコードがあるのですが、そのplay();が終了したことを検知して、動作する構文となっています。

そして、その構文の中に音声を連続で再生するためのアルゴリズムを入れたら上手く解決できました。

終わりに

つたない説明で申し訳ありません。
Qiitaへの記事を書きながら、自分の説明力不足を痛感しました・・・。

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

花粉症LINE BotからのデータをWEBカレンダーに表示する(花粉カレンダー作成④)

概要

耳鼻咽喉科の開業医をしています。
花粉症の患者さんに使ってもらえるような花粉飛散情報が分かるカレンダーアプリを作りたいと思っています。
これまでカレンダーを表示して予定を入れることと、ユーザー認証の実装、LINEのデータをFirebaseに貯めるところまで行ってきました。
Vue.js×FullCallendarでWEBカレンダー作成(花粉カレンダー作成①)
Auth0で簡単にユーザー認証を実装(花粉カレンダー作成②)
花粉症LINE Botのデータをnode.jsを使ってFirebaseに出し入れする(花粉カレンダー作成③)

今回はLINEBotのデータが記録されているFirebaseのdatabaseのデータをカレンダーに表示することに挑戦しました。

LINEBotの記事はこちら 
花粉症の重症度を判定し自分に合う市販薬を教えてくれるLINE Botの作成

完成動画

https://youtu.be/FKGfKFKBq_U

作成

1.FirebaseのRealtime Databaseの確認

LINEのデータはFirebaseのRealtime Databaseに記録されています。
データは以下のように収納されています。

image.png

今回は以下の情報を取得して重症度や薬剤名、緯度経度をリアルタイムでカレンダーに記入していきたいと思います。
・postback.data(花粉症の重症度や使用している薬剤の情報)
・postback.params.datatime(重症度判定を行った日や薬剤使用開始した日の情報)
・sorce.userID(LINEのユーザーID)
個別の花粉飛散情報を表示するため
・message.latitude(ユーザー位置情報 緯度)
・message.latitude(ユーザー位置情報 経度)

データは.(ドット)で深堀していくことができるようです。

2.実装

以前作成したCalendar.vueに追記していきます。
Vue.js×FullCallendarでWEBカレンダー作成(花粉カレンダー作成①)

methods: { }の中に以下を追記します。
緯度や経度は本当はデータが取得できるだけでいいのですが、今回は本日の日付で表示してみました。

childAdded(snap) {
      const message = snap.val();
      const mes = message.events[0];

      if (mes.type == "postback") {
        console.log(mes.postback.data);
        console.log(mes.postback.params.datetime);
        console.log(mes.source.userId);        

        this.calendarEvents.push({
          title: mes.postback.data,//重症度や薬剤
          start: mes.postback.params.datetime,
          end: mes.postback.params.datetime
        });
      }
      if (mes.type == "message") {        
        if(mes.message.type=="location"){
        console.log(mes.message.latitude);
        console.log(mes.message.longitude);
        userlat = mes.message.latitude;// 緯度
        userlong = mes.message.longitude;//経度
        };      
        this.calendarEvents.push({
          // title: mes.message.text,
          title: `緯度${userlat}`,
          start: "2020-01-19T09:00:00",
          end: "2020-01-19T10:30:00"
        },
        {
          title:`緯度${userlong}`,
          start: "2020-01-19T09:00:00",
          end: "2020-01-19T10:30:00" 
        }
        );
      }
    },

async created() { }の中に以下を追記して完成です。

 const ref_message = firebase.database().ref("protoout/studio/messageList");
 //新しいメッセージ2件だけ表示する
 ref_message.limitToLast(2).on("child_added", this.childAdded);

LINEから位置情報を送ると緯度と経度が表示されます。
image.png

考察

Firebaseのデータをカレンダーに表示することが出来ました。
次は気象APIから花粉情報を表示できるようにしたいと思います。

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

Zeitの最強ホスティングサービスnowのDNS設定にレコードを追加する方法

今回始めてNext.jsアプリをnowにデプロイしました。

nowでは簡単に独自ドメイン設定ができるので、お名前どっとこむで取得したものを設定しました。
また、SEO対策で欠かせない?Google Search Consoleでのドメイン所有権確認のためにTXTレコードの追加が必要になり、ここで少しつまずいたのでメモ的にDNSレコード追加方法を書いておきます。

ドメイン設定はName Server転送を選択

そもそものドメイン設定は、Zeitがおすすめしているネームサーバー転送で行いました。
now-domain-setting.jpg
つまり、これから自分のドメインにTXT等レコードを追加したい場合は、お名前ドットコムの設定ではなく、nowのものをイジる必要があるわけです。

nowで設定した独自ドメインにTXTレコードを追加してみる

nowは非常にミニマルで美しい管理画面を提供してくれているのですが、今回やりたい、レコードの追加はブラウザではできないようです。ではどうやるのか、コマンドラインツールnowコマンドです。

nowコマンドでTXTレコード追加

npmやyarnでnowコマンドをグローバルインストールした後、

$ npm i -g now # Or yarn global add now

nowにログインします

$ now login

そして以下コマンドで一発完了

$ now dns add sample.com @ TXT "TXTレコードの値"

宣伝

こんにちは。
自分は新卒でヤフー→4年で退職→2019/05よりバンクーバー在住のソフトウェアデベロッパーです。
Node.js/Vue/Nuxt/React/Next 周りならフロントバックエンド共に開発できます。
バンクーバーからのリモートでもOK!という案件お待ちしております!
@taishikat0_Ja
taishikato.com/resume

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

Javascriptのシンプルな構成でAWS Cognitoを理解する

概要

いろいろと理解しなければならないことが多いですが、まずは、できるだけシンプルな構成でAWS Cognitoの基礎を理解します。
jQueryは使わずにJavascriptのみです。
ただし、アカウントの属性には標準属性とカスタム属性を設定します。
今回説明する内容を踏まえて、実用ではAmplifyを使うと良いかと思っています。

画面遷移

まずはサインアップ画面
image.png
サインアップするとメールで検証リンクが配信されます。
image.png
Verify Emailリンクをクリックすると検証が完了します。
image.png

次にサインイン画面
image.png
最後に認証されたアカウントしか入ることのできない画面
image.png
Sign Outをクリックするとサインアウトしてサインイン画面に遷移します。

ファイル構成

image.png

jsフォルダへ準備するライブラリ

それぞれのREAD.MEをしっかり確認してください。

AWS Cognitoの設定

  1. AWSへサインイン
  2. Cognitoのコンソールへ
  3. リージョンを東京へ変更
    image.png
  4. ユーザープールの管理をクリック
  5. ユーザープールを作成するをクリック
    image.png
  6. プール名へお好きな名前を付けてください。
    image.png
  7. デフォルトを確認するをクリックすると↓の画面になります。
    image.png
  8. サイドバーの属性をクリック
  9. 今回は↓のように設定します。
    image.png
    カスタム属性にはroleと入力しますが、自動でcustom:roleと変更されます。
  10. 次のステップをクリック
  11. 「ポリシー」の設定
    image.png
  12. 「MFAそして確認」の設定
    image.png
  13. 「メッセージのカスタマイズ」の設定
    image.png
  14. 「アプリクライアント」の設定でアプリクライアントの追加をクリック
    image.png
  15. アプリクライアント名を入力し、クライアントシークレットを作成のチェックを外す
    image.png
  16. サイドバーで「確認」をクリックすると↓が表示され、プールの作成をクリック
    image.png
  17. プールIDが表示されるので、それをメモ
    image.png
  18. サイドバーから「アプリクライアント」を選択するとアプリクライアントIDが表示されるので、それをメモ
    image.png
  19. サイドバーから「ドメイン名」を選択するとAmazon Cognito ドメインの作成ができるので好きな名前を入力してドメインを作成します。
    image.png
  20. これでユーザープールの設定が完了

フェデレーティッドアイデンティティの設定

  1. Cognitoコンソールの上部にあるフェデレーティッドアイデンティティをクリック
    image.png
  2. image.pngをクリック
  3. 使用開始ウィザードでIDプール名に好きな名前を入力
  4. 認証されていないIDに対してアクセスを有効にするをチェック
  5. 認証プロバイダーを展開し、Cognitoを選択
  6. メモしたユーザープールIDとアプリクライアントIDを入力
    image.png
  7. プールの作成をクリック
  8. 次の画面ではデフォルトの内容を確認して、許可をクリック
  9. サイドバーでサンプルコードを選択し、プラットフォームをJavascriptを選択。AWS認証情報の取得に表示されているコードをメモ
    image.png
  10. これでCognitoの設定が完了

サインアップページの作成

signup.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Sign Up</title>
    <script src="./js/jsbn.js"></script>
    <script src="./js/jsbn2.js"></script>
    <script src="./js/sjcl.js"></script>
    <script src="./js/aws-sdk.min.js"></script>
    <script src="./js/aws-cognito-sdk.min.js"></script>
    <script src="./js/amazon-cognito-identity.min.js"></script>
  </head>
  <body>
    <div id="signup">
      <h1>Sign Up</h1>
      <div id="message"><span id="message-span" style="color: red;"></span></div>
      <form name="form-signup">
        <span style="display: inline-block; width: 150px;">User ID(Email)</span>
        <input type="text" id="email" placeholder="Email Address" />
        <br />
        <span style="display: inline-block; width: 150px;">Name</span>
        <input type="text" id="name" placeholder="Name" />
        <br />
        <span style="display: inline-block; width: 150px;">Password</span>
        <input type="password" id="password" placeholder="Password" />
        <br /><br />
        <input type="button" id="createAccount" value="Create Account" />
      </form>
    </div>
    <br />
    <a href="./signin.html">Sign In!</a>
    <script src="./js/signup.js" defer></script>
  </body>
</html>

サインアップの処理

これまでにメモしたユーザープールIDクライアントIDAWS認証情報を張り付けます。

js/signup.js
(() => {
  // ユーザープールの設定
  const poolData = {
    UserPoolId: "us-east-1_xxxxxxxx",
    ClientId: "xxxxxxxxxxxxxxxxxxxxx"
  };
  const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);

  const attributeList = [];

  // Amazon Cognito 認証情報プロバイダーを初期化します
  AWS.config.region = "us-east-1"; // リージョン
  AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: "us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  });

  // 「Create Account」ボタン押下時
  const createAccountBtn = document.getElementById("createAccount");
  createAccountBtn.addEventListener("click", () => {
    /**
     * サインアップ処理。
     */
    const username = document.getElementById("email").value;
    const name = document.getElementById("name").value;
    const password = document.getElementById('password').value;

    // 何か1つでも未入力の項目がある場合、処理終了
    const message = document.getElementById("message-span");
    if (!username | !name | !password) {
      message.innerHTML = "未入力項目があります。";
      return false;
    }

    // ユーザ属性リストの生成
    const dataName = {
      Name: "name",
      Value: name
    };
    const dataRole = {
      Name: "custom:role",
      Value: "5"
    };
    const attributeName = new AmazonCognitoIdentity.CognitoUserAttribute(
      dataName
    );
    const attributeRole = new AmazonCognitoIdentity.CognitoUserAttribute(
      dataRole
    );

    attributeList.push(attributeName);
    attributeList.push(attributeRole);

    // サインアップ処理
    userPool.signUp(username, password, attributeList, null, (err, result) => {
      if (err) {
        message.innerHTML = err.message;
        return;
      } else {
        // サインアップ成功の場合、アクティベーション画面に遷移する
        alert(
          "登録したメールアドレスへアクティベーション用のリンクを送付しました。"
        );
        location.href = "signin.html";
      }
    });
  });
})();

サインインページの作成

signin.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Sign In</title>
    <script src="./js/jsbn.js"></script>
    <script src="./js/jsbn2.js"></script>
    <script src="./js/sjcl.js"></script>
    <script src="./js/aws-sdk.min.js"></script>
    <script src="./js/aws-cognito-sdk.min.js"></script>
    <script src="./js/amazon-cognito-identity.min.js"></script>
  </head>
  <body>
    <div id="signin">
      <h1>Sign In</h1>
      <div id="message"><span id="message-span" style="color: red;"></span></div>
      <form name="form-signin">
        <span style="display: inline-block; width: 150px;">Email Address</span>
        <input type="text" id="email" placeholder="Email Address" />
        <br />
        <span style="display: inline-block; width: 150px;">Password</span>
        <input type="password" id="password" placeholder="Password" />
        <br /><br />
        <input type="button" id="signinButton" value="Sign In" />
      </form>
    </div>
    <br />
    <a href="./signup.html">Sign Up!</a>
    <script src="./js/signin.js" defer></script>
  </body>
</html>

サインインの処理

これまでにメモしたユーザープールIDクライアントIDAWS認証情報を張り付けます。

js/signin.js
(() => {
  // ユーザープールの設定
  const poolData = {
    UserPoolId: "us-east-1_xxxxxxxx",
    ClientId: "xxxxxxxxxxxxxxxxxxxxxxxxx"
  };
  const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);

  // Amazon Cognito 認証情報プロバイダーを初期化します
  AWS.config.region = "us-east-1"; // リージョン
  AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: "us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  });

  /**
   * サインイン処理
   */
  document.getElementById("signinButton").addEventListener("click", () => {
    const email = document.getElementById('email').value;
    const password = document.getElementById('password').value;

    // 何か1つでも未入力の項目がある場合、メッセージを表示して処理を中断
    const message = document.getElementById('message-span');
    if (!email | !password) {
      message.innerHTML = "All fields are required.";
      return false;
    }

    // 認証データの作成
    const authenticationData = {
      Username: email,
      Password: password
    };
    const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(
      authenticationData
    );

    const userData = {
      Username: email,
      Pool: userPool
    };
    const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

    // 認証処理
    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: result => {
        const idToken = result.getIdToken().getJwtToken(); // IDトークン
        const accessToken = result.getAccessToken().getJwtToken(); // アクセストークン
        const refreshToken = result.getRefreshToken().getToken(); // 更新トークン

        console.log("idToken : " + idToken);
        console.log("accessToken : " + accessToken);
        console.log("refreshToken : " + refreshToken);

        // サインイン成功の場合、次の画面へ遷移
        location.href = "index.html";
      },

      onFailure: err => {
        // サインイン失敗の場合、エラーメッセージを画面に表示
        console.log(err);
        message.innerHTML = err.message;
      }
    });
  });
})();

認証完了で閲覧できるページの作成

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Menu</title>
    <script src="./js/jsbn.js"></script>
    <script src="./js/jsbn2.js"></script>
    <script src="./js/sjcl.js"></script>
    <script src="./js/aws-sdk.min.js"></script>
    <script src="./js/aws-cognito-sdk.min.js"></script>
    <script src="./js/amazon-cognito-identity.min.js"></script>
  </head>
  <body>
    <div id="menu">
      <h1 id="name"></h1>
      <h2 id="role"></h2>
      <p id="email"></p>
    </div>
    <button id="signout" hidden>Sign Out</button>
    <script src="./js/index.js" defer></script>
  </body>
</html>

認証済みかチェックする処理

これまでにメモしたユーザープールIDクライアントIDAWS認証情報を張り付けます。

js/index.js
(() => {
  // ユーザープールの設定
  const poolData = {
    UserPoolId: "us-east-1_xxxxxxxxxx",
    ClientId: "xxxxxxxxxxxxxxxxxxxxxxxxxx"
  };
  const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
  const cognitoUser = userPool.getCurrentUser(); // 現在のユーザー

  const currentUserData = {}; // ユーザーの属性情報

  // Amazon Cognito 認証情報プロバイダーを初期化します
  AWS.config.region = "us-east-1"; // リージョン
  AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: "us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  });

  // 現在のユーザーの属性情報を取得・表示する
  // 現在のユーザー情報が取得できているか?
  if (cognitoUser != null) {
    cognitoUser.getSession((err, session) => {
      if (err) {
        console.log(err);
        location.href = "signin.html";
      } else {
        // ユーザの属性を取得
        cognitoUser.getUserAttributes((err, result) => {
          if (err) {
            location.href = "signin.html";
          }

          // 取得した属性情報を連想配列に格納
          for (i = 0; i < result.length; i++) {
            currentUserData[result[i].getName()] = result[i].getValue();
          }
          document.getElementById("name").innerHTML =
            "ようこそ!" + currentUserData["name"] + "さん";
          document.getElementById("role").innerHTML =
            "Your Role is " + currentUserData["custom:role"];
          document.getElementById("email").innerHTML =
            "Your E-Mail is " + currentUserData["email"];

          // サインアウト処理
          const signoutButton = document.getElementById("signout");
          signoutButton.addEventListener("click", event => {
            cognitoUser.signOut();
            location.reload();
          });
          signoutButton.hidden = false;
          console.log(currentUserData);
        });
      }
    });
  } else {
    location.href = "signin.html";
  }
})();

動作のテスト

一連の流れをブラウザでテスト

  1. サインアップ
  2. メールでアクティベート
  3. サインイン
  4. 認証情報の表示
  5. サインアウト

Cognitoのコンソールでユーザーの状況が確認できる

image.png

ユーザーを無効化したり、削除したり、詳細な操作ができる

image.png

最後に

他にもパスワードの変更や再設定など必要な機能があるかと思いますが、入門としてはシンプルに作成できたかと思います。
ここからJWTや認証・認可などへ広げていけば良いかと思います。

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

鼓膜の画像を送り質問に返答すれば、自動で中耳炎の診断や治療方針が返されるLINE Botを作成(ヒーローズ・リーグ2019 LINEテーマ賞)

概要

耳鼻咽喉科の開業医をしています。

以前、質問に答えていくと急性中耳炎の重症度が分かるLINE Botと
鼓膜画像を送ると正常か中耳炎かを答えてくれるLINE Botを作成しました。

急性中耳炎の重症度が分かるLINE Botの作成
Microsoft Custom Vision Serviceによる中耳炎画像認識LINE Botの作成

今回、二つのBotを組み合わせて、鼓膜の画像を送り質問に返答すれば、自動で中耳炎の診断や治療方針が返されるLINE Botを作成しました。

概念図

image.png

完成動画/画像

image.png

IMG-0982.PNG

作成

以前の作成したBotのコードを変えていきます。
Azure Custom Vision ServicesのPrediction APIの発行の仕方もこちらの記事を参考にして下さい。

Microsoft Custom Vision Serviceによる中耳炎画像認識LINE Botの作成

まず、ユーザーから送られてくるのがメッセージか画像かで処理を分けます。

function handleEvent(req, res) {
    if (req.body.events[0].type === 'message' && req.body.events[0].message.type === 'text') {
      return handleTextEvent(req.body.events[0]);
    }else if(req.body.events[0].message.type === 'image'){
      return handleImageEvent(req.body.events[0]);
    }
    console.log("サポートされていないメッセージです");
}

鼓膜画像が送られてきたときの処理です。
最も確率が高い診断名とその確率が表示されます。
診断が急性中耳炎の場合は重症度判定に必要な「鼓膜の発赤」「鼓膜の腫脹」「耳漏」の程度を確率で表示し重症度スコアを計算します。
その後年齢に関する質問が開始され、クイックリプライで表示されます。

function handleImageEvent(event) {
  console.log("画像が来たよ");
  // ユーザーがLINE Bot宛てに送った写真のURLを取得する
  const options = {
    url: `https://api.line.me/v2/bot/message/${event.message.id}/content`,
    method: 'get',
    headers: {
        'Authorization': 'Bearer 自分のchannelAccessToken'  ,
    },
    encoding: null
  };

Request(options, function(error, response, body) {

    if (!error && response.statusCode == 200) {
        //保存

        console.log(options.url + '/image.jpg');
        let strURL = options.url + '/image.jpg';

        //Nowでデプロイする場合は、/tmp/のパスが重要
        fs.writeFileSync(`/tmp/` + event.message.id + `.png`, new Buffer(body), 'binary');

        const filePath = `/tmp/` + event.message.id + `.png`;

//Azure Custom Vision APIの設定
const config = {
  "predictionEndpoint": "ひかえておいたURL",
  "predictionKey": 'ひかえておいたKey'
  };

  let result1;

  cv.sendImage(
      filePath,
      config,
      (data) => {
        console.log(data); 
          let result0="";
          // let result1;
          let result2 = "";
          let result3 = "";
          let result4 = "";
          let result5 = "";
          let strName = "";
          let Probability ;
          let strProbability;        

          for (var i = 0; i <4; i++) {
            strName = data.predictions[i].tagName;
            Probability = data.predictions[i].probability * 100;
            strProbability = Probability.toFixed();
              if (strName == "急性中耳炎") {
                result1 = "急性中耳炎";
                result0 = "ですね。\n確率は"+strProbability + '%\n\n';
              }else if (strName == "滲出性中耳炎") {
                result1 = "滲出性中耳炎";
                result0 = strProbability + '%';
              }else if(strName == "正常鼓膜") {
                result1 = "正常鼓膜"; 
                result0 = strProbability + '%';
              }
          }

          let symptoms = {};
          let score = 0;
        if (result1 == "急性中耳炎") {
          for (var i = 0; i < 10; i++) {
            strName = data.predictions[i].tagName;
            Probability = data.predictions[i].probability * 100;
            strProbability = Probability.toFixed();
            if (symptoms["発赤"] === undefined) {
              if (strName == "発赤:なし") {
                symptoms["発赤"] = "発赤なし" + strProbability + '%,\n';
                //score0
              } else if (strName == "発赤:一部") {
                symptoms["発赤"] = "発赤一部" + strProbability + '%,\n';
                score += 2;
              } else if (strName == "発赤:全体") {
                symptoms["発赤"] = "発赤全体" + strProbability + '%,\n';
                score += 4;
              }
              result2 = symptoms["発赤"];
            }

            if (symptoms["腫脹"] === undefined) {
              if (strName == "腫脹:なし") {
                symptoms["腫脹"] = "腫脹なし" + strProbability + '%,\n';
              } else if (strName == "腫脹:一部") {
                symptoms["腫脹"] = "腫脹一部" + strProbability + '%,\n';
                score += 4;
              } else if (strName == "腫脹:全体") {
                symptoms["腫脹"] = "腫脹全体" + strProbability + '%,\n';
                score += 8;
              }
              result3 = symptoms["腫脹"];
            }

            if (symptoms["耳漏"] === undefined) {
              if (strName == "耳漏:なし") {
                symptoms["耳漏"] = "耳漏なし" + strProbability + '%,\n';
              } else if (strName == "耳漏:あり") {
                symptoms["耳漏"] = "耳漏あり" + strProbability + '%,\n';
                score += 2;
              }
              result4 = symptoms["耳漏"];

            }
          }
          // }


          client.replyMessage(event.replyToken, {

              "type": "text", // ①
              "text": result1 + result0 + result2 + result3 + result4 + "➡重症度スコア:" + String(score)+"\n\nいくつか質問にお答えください。\n\n2歳未満ですか?",
              "quickReply": {
                "items": [
                  {
                    "type": "action",
                    "action": {
                      "type": "message",
                      "label": "いいえ",
                      "text": "2歳以上 トータルスコア:" + String(score)
                    }
                  },
                  {
                    "type": "action",
                    "action": {
                      "type": "message",
                      "label": "はい",
                      "text": "2歳未満 トータルスコア:" + String(score + 3)
                    }
                  }

                ]
              }            
          });

        } else if (result1 == "滲出性中耳炎") {
          client.replyMessage(event.replyToken, {
            type: 'text',
            text: result1 + "ですね。\n確率は" + result0  ,
          });
        } else if (result1 == "正常鼓膜") {
          client.replyMessage(event.replyToken, {
            type: 'text',
            text: result1 + "ですね。\n確率は" + result0 ,
          });
        }
          try {
                    fs.unlinkSync(filePath);
                    return true;
                  } catch(err) {
                    return false;
                  }
                return; 
            },
            (error) => { console.log(error) }
        );
    } else {
        console.log('imageget-err');
    }
});
}

メッセージに対する処理は、function handleTextEvent(event) { }の中に
急性中耳炎の重症度が分かるLINE Botの作成のLINE botのプログラムを入れて追記すると完成です。

質問に対するクイックリプライの回答から重症度スコアを加算していき、すべての質問が終わるとトータルスコアから急性中耳炎の重症度を判定し、ガイドラインで推奨されている治療を返します。

考察

鼓膜の画像さえきれいに撮影できれば、高い精度で急性中耳炎のガイドラインに沿った診断と推奨治療を返すBotを作成できました。

こちらのBotで昨年末に開催された開発コンテストのヒーローズ・リーグ2019で賞(LINEテーマ賞 by LINE株式会社様)をいただきとても嬉しかったです。

また、先日耳鼻咽喉科の学術講演会でこのBotについても発表させていただき耳鼻咽喉科の先生方からもかなり反響がありました。

鼓膜の撮影をするカメラ(デジタル耳鏡)は通販で3~4000円で購入できるため、一般の方が自宅で撮影することもできるのですが、Botが病気の診断することは現在の法律で禁じられているため、こちら公開して使って頂くことは出来ません。データを増やし精度を上げながら自院で医師の指導のもと中耳炎の再来患者さんを中心に使用していただいて、有効性や安全性を検証していきたいと思っています。

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

【初心者】JavaScriptで「コマ移動ゲーム(横移動のみ)」を作ってみた

最近、JavaScriptを勉強しています。
練習として、「コマ移動ゲーム(横移動のみ)」を作ってみました。
簡単なものですが、アウトプットが大切だと考えています。笑

HTMLを記述する

index.html
<h1>コマ移動ゲーム(横移動のみ)</h1>
<p id="field"></p>
<input type="button" value="左" id="btn_left">
<input type="button" value="右" id="btn_right">
<input type="button" value="リセット" id="btn_reset">

<script src="main.js"></script>

タイトル、コマが移動するフィールド、3つのボタンという構成です。
フィールドの描画は、JavaScriptにお任せしているので、要素の中身は空です。
JavaScriptの読み込みも忘れずにしています。

JavaScriptを記述する

main.js
const field = document.getElementById('field');
const btn_left = document.getElementById('btn_left');
const btn_right = document.getElementById('btn_right');
const btn_reset = document.getElementById('btn_reset');
const loopCount = 10;
let position = 0;

const fieldReset = (reset) => {
  if (reset) {
    position = 0;
  }

  field.textContent = '';
};

const draw = () => {
  for (let i = 0; i < loopCount; i++) {
    if (position === i) {
      field.textContent += '';
    } else {
      field.textContent += '';
    }
  }
};

draw();

btn_left.addEventListener('click', () => {
  fieldReset();

  if (position > 0) {
    position--;
  }

  draw();
});

btn_right.addEventListener('click', () => {
  fieldReset();

  if (position < loopCount - 1) {
    position++;
  }

  draw();
});

btn_reset.addEventListener('click', () => {
  fieldReset('reset');
  draw();
});

最初にフィールドとボタンのHTML要素を取得し、
フィールドの数とコマの位置を示す値を定義しています。
loopCount はフィールドの数(動ける範囲)なのですが、
ちょっと定数名が分かりにくいですね、反省します。

draw() について

const draw = () => {
  for (let i = 0; i < loopCount; i++) {
    if (position === i) {
      field.textContent += '';
    } else {
      field.textContent += '';
    }
  }
};

関数 draw は、フィールドを描画してくれます。
コマの位置のみ で、その他は で表現しています。
フィールドのHTML要素内の文字列を足していく、という形で実現しています。

fieldReset(reset) について

const fieldReset = (reset) => {
  if (reset) {
    position = 0;
  }

  field.textContent = '';
};

関数 fieldReset(reset) は、フィールドをリセットしてくれます。
引数に何かしら与えられた場合のみ、コマの位置もリセットしてくれます。
フィールドのHTML要素内の文字列を空にする、という形で実現しています。

3つのボタンについて

それぞれのボタンが押されたときの挙動を説明します。

  • 「左」:フィールドをリセットし、変数 position-1 して再描画
  • 「右」:フィールドをリセットし、変数 position+1 して再描画
  • 「リセット」:フィールドをリセットし、変数 position0 にして再描画

その他

getElementByIdaddEventListener 、アロー関数 () => { ~ }
などの、その他の基本的な知識については、僕の過去記事をどうぞ。
【初心者】JavaScript(ES6)の基礎を勉強した
【初心者】JavaScriptで「おみくじゲーム」を作ってみた

おわりに

「おみくじゲーム」に引き続き、JavaScriptでミニゲームを作ってみました。
簡単なものですが、アウトプットは楽しいですし、
Qiitaの記事を書くと個人的な復習にもなって、より深い理解につながりますね。

引き続き、アウトプット重視でプログラミング学習がんばります。
Twitter フォローしてくれたら喜びます。

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

JSDocの書き方・出力メモ

最近はJavaScriptを書くことが多く、「仕様書出せ!」と言われるのでJSDocでの記述・出力メモ。

やりたいこと

  • ./srcフォルダに機能別に保存され、module.exportsされる関数の仕様を記述・出力する

セットアップ

めんどくさいのでグローバルにインストールします。

npm install -g jsdoc

サンプル

作業場所の準備

mkdir jsdoc-test
cd jsdoc-test
mkdir src
touch hello.js bye.js

hello.jsの記述

モジュールの場合は@moduleを付けてやらないと何も出力されない。

hello.js
/**
 * Helloという文字列を返します。
 * @module hello
 * @param {string} name - 表示したい名前を指定する。
 * @return {string} - [Hello + name]という形式で戻る。
 */
module.exports = hello = (name) => {
    return "Hello" + name;
}

bye.jsの記述

bye.js
/**
 * Helloという文字列を返します。
 * @module bye
 * @param {string} name - 表示したい名前を指定する。
 * @return {string} - [Bye + name]という形式で戻る。
 */
module.exports = bye = (name) => {
    return "Bye" + name;
}

Doc出力

jsdoc src

すると、同じ階層にoutというフォルダが作成される。その中のindex.htmlを見る。
下記のような内容が表示される(味気ない)。

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

右メニューのHelloを見ると下記のような感じ。

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

基本は以上。

応用1:Home画面に説明文を入れたい

Markdownで説明文を書いて、出力時に指定すれば取り込んでくれるみたい(便利)。

README.md
### テストAPIです。

以下の2つを作りました。

* hello()
* bye()

そして、

jsdoc src README.md

とすると、下記のようにHomeに出力されます。

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

応用2:Objectを記述する

JSではObjectを引数、戻り値に取ることもありますよね。。。下記のように記述するようです。

object.js
/**
 * Objectを受け取りObjectを返します。
 * @module object
 * @param {Object} user - ユーザーオブジェクト
 * @param {string} user.name - ユーザの名前。
 * @param {number} user.age - ユーザーの年齢。
 * @return {Object} result オブジェクトが戻る。
 * @return {string} result.status - "OK or NG"。
 * @return {string} result.message - メッセージが返ります。
 */
module.exports = object = (User) => {
    return { status: "OK", message: "hoge" }
}

が、returnはうまく表現できないのかな・・・。

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

応用3:テンプレートを変更する

しかしJSDocの標準テンプレートはなんでこんなに微妙なんだろ。。。せめて左メニューにしてくれたらいいのに。。。
変え方はひとまずリンクだけ。

こちらのサイトに紹介されているので一旦メモ。

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

【Rails】 DataTables 動的にカラムを変更する方法

はじめに

Railsアプリケーションで DataTables を使う方法を以前にまとめさせていただきました。
DataTables を使ったテーブルのカラムを動的に変更したい需要があると思いますが、まとめられている記事を見かけませんでしたので、こちらにてまとめさせていただきます。
この方法を理解していれば、開発時間を極端に減らして高機能なテーブルを提供することができます。

なお、今回の方法は少し強引にカラムを動的に変更しています。
もし、もっといい方法があるということをご存知の方はコメントをいただければ嬉しいです。

動的にカラムを変更する方法

前提条件

下記リンクにてDataTablesを実装していること。
このリンク先のコードを元にして、動的にカラムを変更する方法をこちらの記事にて特記したいと思います。

値を変更する方法

動的にカラムを変更するには、users.coffeeのコードを変更することによって可能です。

ここでは一例として、id が 3 のときに"あほ"を表示するコードを書いています。

app/assets/javascripts/users.coffee
$ ->
  # *** 省略 ***

  # user_table へカラムを追加する
  user_table.setColumns([

    # 以下に注目
    { 
      data: 'id',         title: 'ユーザID', width: '5%'
      # 以下を追記
      render: (data, type, row) ->
        # data / type / row にどんなデータが入っているか確認。
        console.log data
        console.log type
        console.log row

        if data == "3"
          "あほ"
    },
    { data: 'username',   title: 'ユーザ名', width: '25%' },
    { data: 'name',       title: '名前',    width: '30%' },
    { data: 'created_at', title: '登録日時', width: '20%' },
    { data: 'updated_at', title: '更新日時', width: '20%' },
  ])
  # *** 省略 ***

フォントを変更する方法

つづいて、フォントを変更する方法を紹介します。

一例として、id が 3 のときに赤色の文字で"あほ"と表示するコードを書いています。
htmlタグを使ってそこにcssを埋め込んでいるだけとなります。(少し強引かもしれません。。)

app/assets/javascripts/users.coffee
$ ->
  # *** 省略 ***
    # 以下に注目
    { 
      data: 'id',         title: 'ユーザID', width: '5%'
      # 以下を追記
      render: (data, type, row) ->
        if data == "3"
          "<span style='color: red;'>あほ</span>"
    },
  # *** 省略 ***

Bootstrap のレイアウトを導入する

最後に応用技として、 Bootstrap のレイアウトを導入する方法を紹介します。

一例として、id が 3 のときに赤色の文字で"あほ"と表示するコードを書いています。
応用と書きましたが、htmlタグを使ってbootstrapで使用できるclassを付与しているだけとなります。

app/assets/javascripts/users.coffee
$ ->
  # *** 省略 ***
    # 以下に注目
    { 
      data: 'id',         title: 'ユーザID', width: '5%'
      # 以下を追記
      render: (data, type, row) ->
        if data == "3"
          "<div><center><span class='label label-default'>あほ</span></center></div>"
    },
  # *** 省略 ***

まとめ

いかがでしょうか。Railsなので動的に値を変更したい需要はかなりあるかと思いますが、DataTables だとドキュメントが英語で読みづらいし、あまり柔軟性がないと考える方もいると思います。
少し強引ではありますが、このような感じで色々な応用をすることも可能ですので、ご自身で色々と試してみるのもいいかもしれません。

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

花粉症LINE Botのデータをnode.jsを使ってFirebaseに出し入れする(花粉カレンダー作成③)

概要

耳鼻咽喉科の開業医をしています。

今回、以前作成したLINE Botのデータをnode.jsを使ってFirebaseに出し入れできるようにしました。

以前作ったLINE Botの記事はこちら 
花粉症の重症度を判定し自分に合う市販薬を教えてくれるLINE Botの作成

作成

1.Firebaseno秘密鍵を生成し準備
こちらを参考にします。
サーバーに Firebase Admin SDK を追加する

歯車マークからプロジェクトの設定を選択します
image.png

サービスアカウントに移動し下部にある「新しい秘密鍵の生成」ボタンを押します。
image.png

次に表示される「キーを生成」ボタンを押します。
すると、すぐに生成されてJSONファイルがダウンロードされます。この段階では、長めの文字数のファイル名.jsonになっています。
ダウンロードしたJSONファイルをserviceAccountKey.jsonに名前を変えて配置します。

Realtime DatabaseのページでdatabaseURLを確認します。
image.png

Firebaseコンソールにある、Realtime Databaseの中にあるデータが確認できるページでdatabaseURLを確認します。赤枠のところをメモしておきます。

2.Firebaseにデータを記録できるようにする

Firebase関連のインストール

npm i firebase-admin

花粉症の重症度を判定し自分に合う市販薬を教えてくれるLINE Botの作成で作成したプログラムに追記していきます。

// Firebase /////////////////////////////////

var admin = require("firebase-admin");

// 1. サービスアカウント鍵を生成しserviceAccountKey.jsonにリネーム
var serviceAccount = require("./serviceAccountKey.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  // 2. Realtime DatabaseのページでdatabaseURLを確認して反映
  databaseURL: "https://*************.com"
});

var db = admin.database();
var ref = db.ref("protoout/studio");
var usersRef = ref.child("messageList");

// LINE /////////////////////////////////////

const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const PORT = process.env.PORT || 3000;

const config = {
    channelSecret: '********************',
    channelAccessToken: '***********************'
};

const app = express();

// app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);

    //ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。
    if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
        res.send('Hello LINE BOT!(POST)');
        console.log('疎通確認用');
        return; 
    }

  // Firebaseにも応答を記録 push
  usersRef.push({
    events:req.body.events
  });  

    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));  

});


3.Firebaseからデータを受信できるようにする

以下のコードを追記します。

// Firebase Listからデータを受信 child added on
var refMessageList = db.ref("protoout/studio/messageList");
refMessageList.on('child_added', function (snapshot) {
    //postbackの場合
    if (snapshot.val().events[0].type == 'postback') {
      console.log('child_added', snapshot.val().events[0].postback.data)
      userpostback = snapshot.val().events[0].postback.data;
    }
    //messageの場合
    if (snapshot.val().events[0].type == 'message') {
      //message-位置情報の場合
      if (snapshot.val().events[0].message.type == 'location') {
        userlat = snapshot.val().events[0].message.latitude;
        userlong = snapshot.val().events[0].message.longitude;
        getweather();//気象情報をreturnする関数
      }     
    }    
   })

4.データベースを確認する
LINEのデータが記録されています。
image.png

考察

LINE BotのデータをFirebaseに出し入れすることができました。
今度はFirebaseのデータをWEBカレンダーに表示できるようにしたいと思います。

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

Svelteでテストがしたい

Svelte 良いですよね。

情報が少なかったので、Store/コンポーネント/E2Eでテストする方法をまとめました。手探りでやっているので、うまく行かないところなどがありましたら、コメントまでお願いいたします。

GitHub
https://github.com/nishinoshake/svelte-minimal-testing

Svelteの環境構築

sveltejs/templateを持ってくる。

npx degit sveltejs/template svelte-app
cd svelte-app
npm install
npm run dev

これで「HELLO WORLD!」と表示されるはずです。
http://localhost:5000

テスト用に少し変更

minimal.gif
最小限のカウント

最小限のテストができそうな形に修正します。
ボタンをカウントアップするだけのシンプルなコンポーネントです。

src/main.js
import App from './App.svelte'

const app = new App({ target: document.body })

export default app
src/stores.js
import { writable } from 'svelte/store'

export const count = writable(0)
export const increment = () => count.update(count => count + 1)
src/App.svelte
<script>
    import { count, increment } from './stores.js'
</script>

<button on:click={increment}>{$count}</button>

Storeのテスト

まずは一番テストしやすそうな状態管理の部分から。

Jestをインストール

ドキュメントの通りにインストールします。ついでにBabelも。
https://jestjs.io/docs/ja/getting-started

npm install -D jest babel-jest @babel/core @babel/preset-env
babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current'
        }
      }
    ]
  ]
}
jest.config
module.exports = {
  verbose: true,
  transform: {
    "^.+\\.js$": "babel-jest"
  }
}
package.json
{
  "scripts": {
    "test:unit": "jest test/unit"
  }
}

テストを作成

Storeの値はget()で取得できます。

test/unit/stores.test.js
import { get } from 'svelte/store'
import { count, increment } from '../../src/stores'

test('インクリメント', () => {
  expect(get(count)).toBe(0)

  increment()

  expect(get(count)).toBe(1)
})

テストを実行

npm run test:unit

store.png

コンポーネントのテスト

次にコンポーネントのテストを。

テストツールのインストール

コンポーネントのレンダリングなどをやってくれるツールをインストールします。svelte-testing-libraryというのを使いました。Jestでテストするために必要なパッケージも合わせいれてます。

npm install -D @testing-library/svelte @testing-library/jest-dom jest-transform-svelte

Jestの設定も少し変更します。

jest.config.js
module.exports = {
  verbose: true,
  transform: {
    "^.+\\.js$": "babel-jest",
    "^.+\\.svelte$": "jest-transform-svelte"
  },
  "moduleFileExtensions": ["js", "svelte"],
  "setupFilesAfterEnv": ["@testing-library/jest-dom/extend-expect"]
}

テストを作成

test/unit/App.test.svelte
import { render, fireEvent } from '@testing-library/svelte'
import App from '../../src/App.svelte'

test('コンポーネントのインクリメント', async () => {
  const { container } = render(App)
  const button = container.querySelector('button')

  expect(button.textContent).toBe('0')

  await fireEvent.click(button)

  expect(button.textContent).toBe('1')
})

テストを実行

npm run test:unit

component.png

E2Eテスト

ツールはCypressにしました。

Cypressをインストール

テストサーバーの起動を待つ、start-server-and-testという便利なパッケージも合わせてインストールしてます。

npm install -D cypress start-server-and-test
package.json
{
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public",
    "cy:run": "cypress run",
    "test:unit": "jest test/unit",
    "test:e2e": "start-server-and-test start http://localhost:5000 cy:run"
  }
}

Cypressの設定

デフォルトではプロジェクト直下の cypress ディレクトリを見に行くのですが、testにまとめたかったので少し調整したのと、サーバーのURL指定も追加してます。

cypress.json
{
  "video": false,
  "baseUrl": "http://localhost:5000",
  "fixturesFolder": "test/e2e/fixtures",
  "integrationFolder": "test/e2e/integration",
  "screenshotsFolder": "test/e2e/screenshots",
  "videosFolder": "test/e2e/videos",
  "pluginsFile": false,
  "supportFile": false
}

テストの作成

test/e2e/index.test.js
it('E2Eのインクリメント', () => {
  cy.visit('/')
  cy.get('button').should('have.text', '0')
  cy.get('button').click()
  cy.get('button').should('have.text', '1')
})

アプリケーションのビルド

ビルドしたものに対してテストしたいので事前に。

npm run build

テストの実行

npm run test:e2e

e2e.png

おわり

別の記事にSvelteの魅力もまとめているのでぜひ。
https://qiita.com/nishinoshake/items/46a64591c6411af68af1

GitHub
https://github.com/nishinoshake/svelte-minimal-testing

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

2020年 ITカンファレンスまとめ

2020年に開催されるITカンファレンス

1月20日時点のまとめです。
随時、更新します。

言語

PyCon JP

Pythonに関するカンファレンス
日程: 8月28日、29日の予定
場所: 大田区産業プラザPiO
公式サイト

PHPカンファレンス

PHPに関するカンファレンス
日程: 10月11日
場所: 大田区産業プラザPiO
2019公式サイト
2020公式サイト

PHPerKaigi

PHPに関するカンファレンス
日程: 2月9日 16:30〜
2月10日 10:00〜
2月11日 10:00〜
場所: 練馬区立区民・産業プラザ Coconeriホール
2020公式サイト

JSConf JP

JavaScriptに関するカンファレンス
開催不明
去年
日程:11月30、12月1日
場所:アーツ千代田 3331
2019公式サイト

RubyKaigi

Rubyに関するカンファレンス
日程:4月9日、11日
場所:長野県 まつもと市民芸術館
公式サイト

Go Conference

Goに関するカンファレンス 春、秋開催
開催不明
去年
日程:秋 10月28日
場所:みどりコミュニティセンター
2019公式サイト

YAPC

Perlに関するカンファレンス
日程:3月27日、28日
場所:京都
公式サイト

JJUG

Javaに関するカンファレンス
開催不明
去年
日程:春 5月18日、秋 11月23日
場所:ベルサール新宿グランドコンファレンスセンター
春 公式サイト
秋 公式サイト

TSConf JP

TypeScriptに関するカンファレンス
日程:2月22日
場所:NAVITIME JAPAN
公式サイト

try! Swift

Swiftに関するカンファレンス
日程:3月18日,19日
場所:ベルサール渋谷ファースト
公式サイト

FW

DjangoCongress JP

Djangoに関するカンファレンス
日程: 6月20日
場所: 長野市生涯学習センター
公式サイト

Laravel JP Conference

Larabelに関するカンファレンス
日程:3月21日
場所:グランパークカンファレンス
公式サイト

CakeFest

CakePHPに関するカンファレンス
開催日不明
今年は海外の可能性大
公式サイト

VueFes Japan

Vueに関するカンファレンス
開催不明
去年
日程: 10月12日
場所: TOC五反田メッセ
公式サイト

React Conf Japan

Reactに関するカンファレンス
日程:5月21日
場所:株式会社ナビタイムジャパン
公式サイト

ng-japan

Angularに関するカンファレンス
開催不明
去年
日程:7月13日
場所:Google Tokyo Office
公式サイト

NodeTokyo

Node.jsに関するカンファレンス
開催不明
去年
日程: 10月5日
場所: 丸の内 vacans
公式サイト

スマホ

PWA Night

PWAに関するカンファレンス
日程:2月1日
場所:Abema Towers 10F セミナールーム
公式サイト

iOSDC

iOSに関するカンファレンス
開催不明
去年
日程:9月5日、6日、7日
場所:早稲田大学 理工学部西早稲田キャンパス63号館
2019公式サイト

DroidKaigi

Androidに関するカンファレンス
日程:2月20日、21日
場所:五反田TOCビル 13F
公式サイト

Android Bazaar and Conference

開催不明
去年
日程:5月26日
場所:東海大学 高輪キャンパス
2019公式サイト

その他

AWS SUMMIT TOKYO/OSAKA

日程:5月13日、14日、15日
場所:パシフィコ横浜
日程:6月30日
場所:ホテルニューオータニ大阪
公式サイト

Google Cloud Next

開催不明
去年
日程:7月30日、8月1日
場所:東京プリンスホテル
ザ プリンス パークタワー東京
2019公式サイト

Microsoft Ignite The Tour Tokyo

Microsoftに関するカンファレンス
開催不明
去年
日程:12月5日、6日
場所:ザ・プリンス パークタワー東京
公式サイト

PostgreSQL Conference Japan

開催不明
去年
日程: 11月15日
場所: AP品川9階
2019公式サイト

Adobe MAX Japan

Adobeに関するカンファレンス
日程:11月24日
開催:パシフィコ横浜
公式サイト

Unite Tokyo

Unityに関するカンファレンス
開催不明
去年
日程:9月25日、26日
場所:グランドニッコー東京 台場
2019公式サイト

CEDEC

ゲームに関するカンファレンス
日程:9月2日、3日、4日
場所:パシフィコ横浜 ノース
公式サイト

Object-Oriented Conference

Object指向に関するカンファレンス
日程:2月16日
場所:お茶の水女子大学
公式サイト

VimConf

Vimに関するカンファレンス
開催不明
去年
日程:11月3日
場所:アキバホール
2019公式サイト

Digital Thinkers Conference

デザインに関するカンファレンス
日程:1月23日、24日
場所:イイノホール&カンファレンスセンター
公式サイト

MeetUp

Docker Meetup Tokyo

Dockerに関するミートアップ
公式サイト

Kubernetes Meetup Tokyo

Kubernetesに関するミートアップ
公式サイト

Twitter

最近、Twitter始めました。
ぜひ友達募集中です!
https://twitter.com/apasn1

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

JavaScriptの正規表現

正規表現記号 意味
. 任意の一文字
+ 直前の文字を1回以上繰り返す最長の文字列
* 直前の文字を0回以上繰り返す最長の文字列
? 直前の文字を0~1回繰り返す最長の文字列
+? 直前の文字を1回以上繰り返す最短の文字列
*? 直前の文字を0回以上繰り返す最短の文字列
?? 直前の文字が0~1回繰り返す最短の文字列
OR
\ 直後の正規表現記号をエスケープ
[ ] 括弧内のいずれか一文字
[^ ] 括弧内の文字列以外
[a-Z] 括弧内のみで使える文字の範囲指定
( )
{n} 直前の文字をn回繰り返す
{n,} 直前の文字を繰り返す最小回数
{n,m} 直前の文字を繰り返す最小回数と最大回数の範囲で最長の文字列
{n,m}? 直前の文字を繰り返す最小回数と最大回数の範囲で最短の文字列
標準エスケープ文字 意味
\t タブ
\r 改行
\n 改行
\d 全ての数字
\D 全ての数字以外の文字
\s 垂直タブ以外の全てのスペース
\S 全てのスペース以外の文字
\w アルファベット、アンダーバー、数字
\W アルファベット以外、アンダーバー以外、数字以外
位置の指定 意味
^ 直後の文字が行頭
$ 直前の文字が行末
< 単語の先頭
> 単語の直継
\b 単語の先頭か末尾
\B 単語の先頭以外か末尾以外
\A ファイルの先頭
\z ファイルの末尾
\G 直前の一致文字列の末尾
置換・変換 意味
\0 一致した文字列全体に置換
\1-\9 一致した文字列の1-9番目に対応する文字列に置換
\l 直後の一文字を小文字に変換
\L...\E 挟まれた文字列を小文字に変換
\u 直後の一文字を大文字に変換
\U...\E 挟まれた文字列を大文字に変換
用例 結果
a....a abcdefa
a*a aa
aaaaaaaa
a.*a aba
abbbbbbbbba
a+a aaa
aaaaaaaaaaa
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ElectronでcontextBridgeによる安全なIPC通信

はじめに

Electronの情報って、検索すると沢山出てくるのに、ところどころみな違っていて見極めが難しいですよね。まだまだ私自身よくわかっていないですが、調べた情報を共有します。

現時点での結論として、セキュアなIPC通信にはcontextBridgeを使おう、ということらしいです。

とはいえ、Electronの状況はversionによってかなり変わるようなので、以下の際内容には注意してください。こちらで検証した時点でのElectronのversionは7.1.9です。

Electronにおけるセキュアな設計とは

前提として、Electronでは、メインプロセスと、webページ画面として動くレンダラープロセスが立ち上がります。最初にelectronコマンドの引数として指定したjsファイル(今回はmain.jsとします)がmainプロセス上で実行され、

$ electron ./main.js

その中でBrowserWindow.loadURL()関数などで読み込まれたhtmlがレンダラープロセス上で起動します(今回はindex.htmlとします)。また、index.html上で読み込まれたjsファイルもレンダラープロセス上で実行されます。

たたき台として、以下のようなコードが最小コードとしましょう。

/* main.js, case 0 (initial) **************************/
const {electron,BrowserWindow,app} = require('electron');
let mainWindow = null;
const CreateWindow = () => {
    mainWindow = new BrowserWindow({width: 800, height: 600});
    mainWindow.loadURL('file://' + __dirname + '/index.html');
    mainWindow.webContents.openDevTools(); 
    mainWindow.on('closed', function() {
        mainWindow = null;
    });
}
app.on('ready', CreateWindow);
<!--index.html, case 0 (initial) -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Test</title>
  </head>
  <body>
    <button id="button1">test1</button>
  </body>
  <script type = "text/javascript">
      //適当なプログラム
      const electron = require('electron');//これがエラーになる
      const {ipcRenderer} = require('electron');//これもエラー
 </script>
</html>

ここで、昔のversionのElectronではレンダラープロセス上でもファイル読み書きなどのnodeの便利なメソッドが使えたわけですが、最近はdefaultでは使えなくなっているそうです。ですので、上記の様にレンダラープロセス上の「適当なプログラム」の部分でrequire('electron')と書いて実行しようとすると、"Uncaught ReferenceError: require is not defined at index.html"のようなエラーメッセージが出ます。

じゃあ、ファイル読み書きなどのnodeの機能はメインプロセス上だけでやろう、という方針を取るにしても、レンダラープロセスからの信号や情報をメインプロセスへ伝える手段がいるわけです。プロセス間の通信はIPC通信としてElectronのAPIが用意されているものの、最低限レンダラープロセス上での通信処理を司るipcRendererが欲しくなります(公式docs)。しかし、requireが使えないのでそれすら取得できません。

どうしましょう。

巷の情報

検索して出てくる情報は以下のようなものが多いです。

  1. nodeIntegration: trueにすればよい。

  2. セキュアにするにはnodeIntegration: falseのままにすべし。

  3. その代わりpreloadを使おう。

  4. preload内で準備したオブジェクトや関数をレンダラープロセスのjsで使うためには、(globalや)windowの変数に追加することでインスタンスを渡そう。

  5. あるversion以降、プロセス間でwindowが同一のオブジェクトではなくなった。よって受け渡しできない。同一オブジェクトにするにはcontextIsolation: falseとしよう。

  6. いやいや、セキュアにするにはcontextIsolation: trueのままにしよう。

  7. contextBridgeを使えば、nodeIntegration: false,contextIsolation: trueでもIPC通信できる[^1][^2]。

巡り巡って、どうやら、7番の方法で解決みたいですが、それ以前の手立ても含めて以下にまとめていきます。

方法1(情報1): nodeIntegration: true

nodeIntegrationというのは、メインプロセスでウィンドウを生成するとき位のオプションで指定します。先のmain.jsにおいて、BrowserWindowの生成部分のコードを以下の様に書き替えます。

/* main.js, case 1 */
// ~略~ //
const CreateWindow = () => {
  mainWindow = new BrowserWindow({width: 800, height: 600, 
                 webPreferences: { 
                   nodeIntegration: true,
                 } 
               });
// ~略~ //

これだけで、レンダラープロセスでrequire関数が使えるようになります。しかし、デバッグコンソールには"Electron Security Warning (Insecure Content-Security-Policy)"というwarningメッセージがでてきて、なにやら危ないようです。XSSの危険が大きいということで、あまりお勧めできないようです。

方法2(情報2-6):preloadを使う

では、nodeintegration: falseとしながら、レンダラープロセスでせめてIPC通信だけでもするにはどうするのか。そこで出てくるのがpreloadで追加jsを先行して読ませる方法です。読ませるjsをpreload.jsとします。このpreload.jsにおいてはnode.jsの機能、つまりrequire関数が使えるので、これをグローバルなオブジェクト変数として記録します。それをレンダラープロセスから使うということになります。コードで書くと、次のようになります。

/* main.js, case 2 */
//ipcMainの追加
const {electron,BrowserWindow,app,ipcMain} = require('electron');
let mainWindow = null;
const CreateWindow = () => {
    mainWindow = new BrowserWindow({width: 800, height: 600,
        webPreferences: { 
            nodeIntegration: false, //ここはfalseのまま
            contextIsolation: false,  //これをfalseに
            preload: __dirname + '/preload.js' //preloadするjs指定
        } });
    mainWindow.loadURL('file://' + __dirname + '/index.html');
    mainWindow.webContents.openDevTools(); 
    mainWindow.on('closed', function() {
        mainWindow = null;
    });
}
app.on('ready', CreateWindow);
//IPCメッセージの受信部(レンダラープロセスから送られる)//
ipcMain.on("msg_render_to_main", (event, arg) => {
    console.log(arg); //printing "good job"
});
/* preload.js, case 2*/
const {ipcRenderer} = require('electron');
window.MyIPCSend = (msg)=>{         
    ipcRenderer.send("msg_render_to_main", msg);
}
<!-- index.html, case 2 -->
<!DOCTYPE html>
<html>
~~略~~
<script type = "text/javascript">
  //適当なプログラム
  const button1 = document.getElementById("button1");
  button1.addEventListener("click", (e)=>{
      window.MyIPCSend("good job");});      
 </script>
</html>

まず、main.jsでは、BrowserWindowの生成のoptionにpreloadcontextIsolationの項目を追加しています。またIPCメッセージの受信部としてipcMain.onを設定しています。

preload.jsではrequireが利用できるので、グローバル変数としてwindow.MyIPCSend(msg)関数を追加し、その中でipcRendererを使ったメッセージ送信の機能を持たせます。ここからメインプロセスのipcMain.onへメッセージを送ります。

index.htmlではボタンを押したときにwindow.MyIPCSend(msg)関数を呼び出します。これはpreload.jsで定義したものですが、グローバルなwindowオブジェクトに保持されているので使えるようです。

このような形でIPCメッセージだけでもやり取りできれば、それで必要な情報を送り、node関連の機能を使った処理は全てメインプロセスへ押し付けてしまうこともできるでしょう。

ところがこの方法でも、contextIsolation: falseが必要です。あるversionからデフォルトではcontextIsolation: trueとなったようです。そしてセキュアにするには、ここもtrueがよいと。しかし、trueとすると、preload.jsから呼び出したwindowと、index.htmlで呼び出すwindowのインスタンスが別物になってしまいます。よって、window.MyIPCSend(msg)関数をindex.htmlから呼び出しても、定義されていない旨のエラーメッセージが出ます。

方法3(情報7):contextBridgeを利用する

さて、nodeIntegration: falseかつcontextIsolation: trueのままでIPC通信する手段として、contextBridgeというElectron APIがあるそうです[^1]。これはElectronで公式に提案されたセキュアなプロセス間通信の実現のためのAPIだそうです(これを見つけた時は、嬉しくて叫んじゃいました)。

コードは次のようになります。

/* main.js, case 3 (final) */
// ~~略~~ ここまでcase2と同じ//
    mainWindow = new BrowserWindow({width: 800, height: 600,
        webPreferences: { 
            nodeIntegration: false, //ここはfalseのまま
            contextIsolation: true,  //trueのまま(case2と違う)
            preload: __dirname + '/preload.js' //preloadするjs指定
        } });
// ~~略~~ 以後もcase2と同じ//
/* preload.js, case 3 (final)*/
const { contextBridge, ipcRenderer} = require("electron");
contextBridge.exposeInMainWorld(
    "api", {
        send: (data) => {
            ipcRenderer.send("msg_render_to_main", data);
        }
    }
);
<!-- index.html, case 3 (final) -->
<!DOCTYPE html>
<html>
~~略~~
<script type = "text/javascript">
  //適当なプログラム
  const button1 = document.getElementById("button1");
  button1.addEventListener("click", (e)=>{
      window.api.send("god job");});      
 </script>
</html>

さて、main.jsは方法2と比べてcontextIsolation: trueに変えただけです。

大きく変わったのはpreload.jsです。electronからオブジェクトcontextBridgeを取り出し、exposeInMainWorld()によってグローバルな関数send()を登録しています。ここで登録した関数は、レンダラープロセスのindex.htmlの中からもwindow.api.send()として呼び出すことができます。

めでたし、めでたし。

注意点

contextBridgeはとっても良さそうなAPIですが、Electronのドキュメント[^3]には次のように書かれています。

"The contextBridge API has been published to Electron's master branch, but has not yet been included in an Electron release."

一応、私の環境のversion7.1.9では使えていますが、いつから使えるようになったのかはちょっと不明なので、気を付けてください。

感想

HTML+Javascriptでブラウザ上だけでほぼ動くものを作ってしまえば、パッケージングはElectronですぐにできると思っていた時期が僕にもありました。。。

この記事がだれかの参考になれば幸いです。とはいえ、なにぶんJavascriptはライト勢なので、間違いもたくさんありそう。ご指摘いただければ大変嬉しいです。

References

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