20191124のNode.jsに関する記事は10件です。

【自分用】Node.jsアプリの設計の手順

Twitterのようなアプリを設計する際

①要件定義

<機能要件>
投稿を作る。
投稿を編集する。
投稿を削除する。
投稿をお気に入りとする。
投稿に対してコメントをする。

などの基本的な機能を定義することを言います。

<非機能要件>
機能に付随する要件やセキュリティ要件

などを定義する。

②用語定義

上記の要件定義であがった用語を定義する。

用語 英語表記 意味    
ユーザー user 投稿の利用者
投稿 tweet 投稿をすること
お気に入り favorite 投稿をお気に入りに登録すること
コメント  comment 投稿に対してコメントをすること

用語の表す対象の意味付けをしっかりすることで、思わぬ実装ミスを防ぐことができます。

③データモデリング

ER図などを使い用語の関係性などを定義する。
用語設計をすることによって、要件に漏れがないか、仕組みに問題がないかをチェックすることができます。

ユーザー1 --------- *投稿1 ---------*お気に入り

④URL設計

内容を表示するページ構成を設計する。
同じページ上に表示できるものは一緒のページをする。

・トップページ/投稿一覧ページ
・自分の投稿/コメント/お気に入りを表示するページ
・投稿を作成するページ
・投稿を編集するページ

ページURL

パス メソッド ページの内容 
/ GET トップページ/投稿一覧ページ
/tweet/:tweetid GET 自分の投稿/コメント/お気に入りを表示するページ
/tweet/new GET 投稿を作成するページ
/tweet/:tweetid/edit GET 投稿を編集するページ

WEBAPIのURL

パス メソッド 処理内容  方法
/tweet POST 投稿をする フォーム
/tweet/:tweetid?edit=1 POST 投稿を編集する フォーム
/tweet/:tweetid?delete=1 POST 投稿を削除する AJAX
/tweet/:tweetid/user/:userid/favorite POST 投稿をお気に入りする AJAX
/tweet/:tweetid/user/:userid/comment POST 投稿にコメントをする AJAX

④モジュール設計

Expressなどのフレームワークを使えばモジュール設計は簡単ですが、
ここではMVCに当てはめてみます。

MVC

モジュール設計はMVCフレームワークを使うと整理がしやすいです。

Model

Modelはデータモデリングのことを指し、データの管理を行うモジュールを当てはめます。
Modelモジュールはmodelsディレクトリの配下におきます。

ファイル名 models/user.js models/tweet.js models/comment.js models/favorite.js
役割 ユーザーの定義と保存 ツイートの定義と保存 コメントの定義と保存 お気に入りの定義と保存
View

Viewは見た目、HTMLなどを生成するテンプレート(pugやEJS)を当てはめます。
Viewはviewsディレクトリの中におきます。

ファイル名 index.pug new.pug layout.pug edit.pug
役割 トップ/投稿一覧モジュール 新規投稿モジュール 基礎モジュール 投稿編集モジュール
Controller

ControllerはModelとViewをコントロールする部分で、Node.jsだとルーティングを指します。

ファイル名 routes/tweet.js routes/comment.js routes/fovorite.js
役割 ツイートの処理 コメントの処理 お気に入りの処理

参考:N予備校 プログラミングコース

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

nvmでdefaultバージョンを設定してもsystemのnodeが使用されてしまう

nvmでnodeのバージョン管理をしたいのだが、nvm alias default v*.*.*でデフォルトのバージョンを設定しても、新しいターミナルを開くとsystemのnode(brewでインストールしたnode)が使用されてしまう問題が起こった。
nvm lsを実行するとdefaultではなく、systemを指してしまうのだ。

$ nvm ls
       v12.13.1
->       system
default -> lts/* (-> v12.13.1)
node -> stable (-> v12.13.1) (default)
stable -> 12.13 (-> v12.13.1) (default)

実行環境

macOS 10.14.6
nvm 0.35.1(node v12.13.1をインストール済み)
brewでインストールしたnode v13.1.0

原因

もともと、yarnをbrewでインストールした時にnodeが一緒にインストールされてしまい、それがターミナルを開くとnvmでインストールしたnodeより優先されていた。
(ちなみにbrewでyarnを入れた理由は、yarnの公式にnpmでyarnをインストールするのはお勧めしないということが書いてあるため。)

解決方法

調べてみると、ここに解決方法があった。

どうやら、下記コマンドでbrewからyarnをアンインストールせずに、nodeだけをアンインストールできるようだ。

brew uninstall node --ignore-dependencies

上記実行後、新しいターミナルを開くと無事nvmのdefaultのnodeバージョンを使うことができるようになった。

$ node -v
v12.13.1
$ nvm ls
->     v12.13.1
default -> lts/* (-> v12.13.1)
node -> stable (-> v12.13.1) (default)
stable -> 12.13 (-> v12.13.1) (default)

なお、参考ページの別のコメントに、.bash_profile.bashrcなどでnvmを読み込んだ後に$PATHを変更していることが原因の場合もあるとの指摘もあったので、この記事の方法で解決できない場合はそちらも確認してみると良いかもしれない。

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

とりあえずnode.jsでES6記法(importなど)を使ってみたい時

概要

とりあえずnode.jsを勉強してみようと思いコードを書き始めました。
その中でimportを使おうと思ったのですがエラーとなってしまいました。

babelなどを使わないといけないのかと思い、いろいろ調べていたのですが実際には簡単にできます。
試しにnode.jsを書いてみたいという場合に便利です。

どうやるのか

方法1

  1. jsファイルの拡張子を、.jsから.mjsに変更
  2. --experimental-modulesオプションを付けて実行する
node --experimental-modules index.mjs

これだけでES6記法でnode.jsで書いたjsファイルを実行することができます。

https://nodejs.org/api/cli.html#cli_experimental_modules

方法2

  1. esmというモジュールをインストールします
yarn add esm

2. 次のように実行します

node -r esm index.js

これで実行できます。

esmの別の実行方法

なおこのesmというモジュールは、esmモジュールのコマンドを使ってプロジェクトを作成するやり方もあります。

yarn create esm

を実行することで、プロジェクトが作成されます。
そのプロジェクトディレクトリの中にmain.jsというjsファイルができ、その中に実際にコードを書いていきます。

そして実行はそのプロジェクトディレクトリの中で普通に

node index.js

とすればES6の記法で実行できます。

とりあえずES6記法を使いたいという場合は、このプロジェクトをいちいち作るという方法はめんどうかもしれませんので、そのような場合は、上で書いたnode -r esmが簡単です。

https://www.npmjs.com/package/esm

※ちなみに yarn の -r オプションは、「require」の意味です。

まとめ

調べてみたら意外に簡単にES6記法を試すことはできるようです。
ただ、node.js自体で用意されている--experimental-modulesは、その名称からもわかるように、「experimental(試験的)」なので、実際にしっかりと作り込んでいく場合は、babel などを使っていく必要があるのかもしれません。

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

Node.js, JavaScript学習まとめ

今回の学習のゴール

  1. Node.jsについて知る
  2. 基本文法を学ぶ
  3. ライブラリを把握する

目次

  1. Node.jsとは
  2. そもそもJavaScriptとは
  3. JavaScriptの基本知識
  4. Node.jsの基本知識
  5. ライブラリの把握
  6. 今後の課題

1. Node.jsとは

スケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動のJavaScript環境
Node.js

  • それぞれの意味
    • スケーラブル : 拡張性が高い
    • 非同期 : 各要求(request)の処理が完了するのを待たずに、それ以降の処理を行う方式
    • イベント駆動 : イベントと呼ばれるアプリや端末上で起きた出来事に対して処理を行うプログラムの実行形式
  • 特徴
    • サーバーサイドで使用できる
    • ノンブロッキングI/Oモデルを採用しており、I/Oの処理を待たずに次の処理を始めることができるので、大量のデータ処理が可能
      • ノンブロッキング : ある処理を行いながら、ほかの処理も同時進行で行えること
      • I/O : Input/Outputの略で、入出力の意

→ 大変理解に苦しんだので...
「Node.jsは、サーバー側の処理をJavaScriptで実装できる」ということは押さえておこう

2. そもそもJavaScriptとは

  • ブラウザに実行エンジンが搭載されたプログラミング言語
    • クライアント側のパソコンやスマートフォンに搭載されているWebブラウザがプログラムを実行する
  • Netscape Navigatorというブラウザ向けに開発され、その後Internet Explorer, Firefox, Chromeなどの主要ブラウザに採用された
  • 特徴

    • ブラウザで動作する
    • 実行エンジンの内部で動的にコンパイルが行われるので、コンパイルしなくとも実行できる
    • 動的型付け言語
  • JavaScript - Wikipedia によると

JavaScriptはプロトタイプベースのオブジェクト指向スクリプト言語であるが、クラスなどのクラスベースに見られる機能も取り込んでいる

  • それぞれの意味
    • オブジェクト指向スクリプト言語
      • オブジェクトを組み立てるように表現して、コンピュータに動作をさせ、script(台本、原稿)のようにプログラムを記述できるプログラミング言語
    • プロトタイプベース
      • 全てのオブジェクトがプロトタイプ(試作品)をベースにして作られているもの
      • プロトタイプと呼ばれるテンプレートをコピーして、新しいオブジェクトが作られるイメージ
    • クラスベース
      • 全てのオブジェクトがクラスをベーシにして作られているもの
      • クラスはオブジェクトを作る設計書のことで、クラスそのものの名前、属性、処理の3つの要素を持つオブジェクトをまとめて定義したもの

js.jpg

3. JavaScriptの基本知識

データ型

  • String : 文字列
  • Number : 数
  • Boolean : 真偽値
  • Null : 値が存在しないまたは無効なオブジェクト
  • Undefined : 値を代入していない変数の値
  • Array : 複数の値を格納可能
  • Object : 基本的に何でも格納可能
// 文字列を整数に変換するメソッド
parseInt('030', 10);    // 第2引数には変換の基数, 30が返される
parseInt('hello', 10);    // NaN(非数  "Not a Number" の略)が返される
// 文字列の操作方法
'hello'.charAt(0); // "h"を返す
'hello, world'.replace('hello', 'goodbye'); // "goodbye, world"を返す
'hello'.toUpperCase(); // "HELLO"を返す

演算子

1 + 1;                          // 数字を加える
'Hello' + 'world';              // 文字列の結合
10 - 1;                         // 減算
2 * 3;                          // 乗算
10 / 2;                         // 除算
var myVariable = 'value';       // 代入
myVariable === 'value';         // 等価 値と型が等しいか真偽値で返す 変数myVariableにvalueが代入されている場合は、trueが返される
!(myVariable === 'value');      // 否定 値と型が等しくないか真偽値で返す
myVariable !== 'value';         //非等価 値と型が等しくないか真偽値で返す
weather === 'sunny' && temperature < 25    // AND 2つ以上の式を1つに繋げそれぞれの式を個別に評価、全てtrueになった場合その式全体がtrueを返す
weather === 'sunny' || temperature < 25    // OR 2つ以上の式を1つに繋げそれぞれの式を個別に評価し、最初にtrueになったところでその式全体をtrueとして返す
x += 5;    // 変数xの値を5増やす x = x+5の意味 複合代入文という

変数

  • varを用いた変数の宣言
var <変数名>;    // 変数の宣言
<変数名> = '';    // 変数に値を割り当て, 変数の値を変更する
var <変数名> = '';
<変数名>;    // 変数の値を取得
  • 変数のスコープは関数単位
function f() {
    var num = 123;
    console.log(num);
    {
    var num = 456;
    console.log(num);
    }
    console.log(num);
}
f();

// 実行結果
123
456
456
  • letを用いた変数の宣言
let <変数名> = '';    // ブロックレベルの変数を宣言
  • 変数のスコープがブロックに限定される
function f() {
    let num = 123;
    console.log(num);
    {
    let num = 456;
    console.log(num);
    }
    console.log(num);
}
f();

// 実行結果
123
456
123

定数

const number = '10';    // 定数の宣言 一度宣言された値は変更不可

配列

// 配列を生成する①
var person = new Array();
person[0] = 'たなか';
person[1] = 'なかむら';
person[2] = 'しぶや';

// 配列を生成する②
var person = ['たなか', 'なかむら', 'さいとう']

// 配列に要素を追加する
person.push(いとう);

条件文 if

var color = 'red'
if (color === 'red') {         // (条件式)がtrueを返した場合、以下の処理が実行される
    alert('好きな色は赤です');    // アラートを使って表示
} else if (color === blue) {
    alert('好きな色は青です');
} else {                       // 2つの(条件式)がfalseを返した場合、elseの後の処理が実行される
    alert('好きな色は黄色です');
}

switchステートメント

var color = 'red'
switch (color) {
  case 'red':
    alert('好きな色は赤です');
    break;    // 値がcaseにマッチした時ループを抜ける

  case 'blue':
    alert('好きな色は青です');
    break;

  // 以下に選択肢を好きなだけ並べることが可能

  default:
    alert('選択肢に好きな色がありません');
}

ループ

  • カウンター : ループの開始地点で、初期化される値
  • 終了条件 : ループが終了する条件
  • イテレーター : 終了条件を満たすまで、カウンターの値をループごとに少量ずつ増加(減少)させる
// forを使ったループ
var sequence = [1, 2, 4, 7, 11, 16, 22];
for (var i = 0; i < sequence.length; i++) {    // カウンター変数を宣言 lengthプロパティを使用して配列の長さを取得し、ループを配列の長さと同じ数になったら、繰り返しを終了
  console.log(sequence[i]);
}
// whileを使ったループ
var sequence = [1, 2, 4, 7, 11, 16, 22];
var i = 0;    // 初期化処理
while (i < sequence.length) {
    console.log(sequence[i]);

    i++;
}
// do...whileを使ったループ
var sequence = [1, 2, 4, 7, 11, 16, 22];
var i = 0;
do {
    console.log(sequence[i]);

    i++;
} while (i < sequence.length)

関数

  • 再利用したい機能をパッケージ化する方法
function sayHello() {
  alert('hello');
}

sayHello();    // functionの呼び出し helloのアラートが表示される
function sum(num1, num2) {    //関数の定義 関数に複数の引数がある場合はカンマで区切る
    var result = num1 + num2;
    return result;
}
sum(1, 2);    # コンソールで実行すると3が返ってくる

イベント

  • ブラウザの中で起きていることを検出し、その応答としてコードを実行する
  • 動作や操作(以下の例ではクリック)に対して特定の処理を与えるための命令のことをイベントハンドラという
  • ブラウザに組み込まれたJavaScript APIの一部として定義されたもの
var page = document.selector('html');    // 関数を定義し変数に代入
page.onclick = function() {    // 無名関数は主にイベント発火のレスポンスとして、一連のコードを走らせるだけのような場合に、イベントハンドラとして使われる
    alert('ページがクリックされた');
};

オブジェクト

  • 関連のあるデータと機能をひとまとめにしたモノ
  • 機能はたいてい変数と関数で構成され、オブジェクトの中ではそれぞれプロパティとメソッドと呼ばれる
var obj = new Object();    // 空のオブジェクトを作成する方法①
var obj = {};    // 空のオブジェクトを作成する方法②オブジェクトリテラル構文
  • オブジェクトリテラル使用してオブジェクトを生成する例
var person = {
  name: ['たかはし', 'なかむら'],
  age: 20,
  gender: 'female',
  greeting: function() {    // オブジェクトのメソッド
    alert('こんにちは、' + this.name[1] + 'と申します。' + this.age + '歳です。');    // thisは現在のオブジェクトを参照しているので、personを指す
  }
};
# コンソールで実行

person    # {name: Array(2), age: 20, gender: "female", greeting: ƒ} と返ってくる
person.name[1]    # "なかむら" と返ってくる
person.greeting()    # こんにちはなかむらと申します。20歳です。 とアラートが返ってくる
person.age = 30;    # 値を上書きすることができる
// サブ名前空間でオブジェクト生成するときの記載方法

name: {
  first: 'たかはし',    // name.firstで"たかはし"が返ってくる
  second: 'なかむら'    // name.secondで"なかむら"が返ってくる
}

継承

  • ”親”クラスからの機能を継承する”子供”のオブジェクトクラス (コンストラクタ) の生成方法について
// コンストラクタ内部にプロパティのみを定義
function Person(first, second, age, gender) {
    this.name = {
    first,
    last
  };
  this.age = age;
  this.gender = gender;
};

// メソッドはすべてコンストラクタのプロトタイプとして定義する
Person.prototype.greeting = function() {
  alert('こんにちは、' + this.name.first + 'と申します。' + this.age + '歳です。');
};

// Personコンストラクタの子であるTeacherコンストラクタを作成
function Teacher(first, second, age, gender, subject) {
  Person.call(this, first, second, age, gender);    // call()関数 その他の場所で定義された関数から呼ぶことができる

  this.subject = subject;    // Teacherだけが持つプロパティを定義
}

JSON(JavaScript Object Notation)

  • JavaScript オブジェクトの構文に従ったテキストベースのフォーマット
  • ウェブアプリケーションでデータを転送する場合に使われる
  • MIME type(メディアタイプ)がapplication/jsonで、「.json」という拡張子の付いたテキストファイルとしてJSON自身を格納することもできる(以下その例)
{
  "companyName": "Super heroes",
  "homeTown": "Central City",
  "formed": 2005,
  "active": true,
  "members": [
    {
      "name": "Takahashi",
      "age": 28,
      "business description": [
        "labor management",
        "Payroll"
      ]
    },
    {
      "name": "Nakamura",
      "age": 35,
      "business description": [
        "Disclosure",
        "Payment",
        "Sales recording"
      ]
    }
  ]
}
  • このオブジェクトをJavaScriptのプログラムへ読み込む(excellenceという変数に代入したとすると)と、ドットや角括弧を使ってデータへアクセスすることができる ※JSONでは文字列とプロパティ名をシングルクォートではなく、ダブルクォートで括る
excellence.companyName
excellence['members'][1]['business description'][0]    // 2番目のメンバーの1番目の業務内容を参照

Web API

  • Application Programming Interfacesの略
  • 開発者が複雑な機能を簡単に作成できるように、プログラミング言語から提供される構造のこと
  • ブラウザやサイトが動作しているOSの様々な面を操作したり、他のWebサイト、サービスから取得したデータを操作するためのプログラムされた機能である
  • APIのカテゴリ
    • ブラウザAPI : Webブラウザに組込まれているAPIで、ブラウザやコンピュータの環境の情報を取得して複雑な機能を簡単に実装できる(ex. Geolocation API)
      • ブラウザで読み込んだ文書を操作するためのAPI, サーバからデータ取得をするAPI, クライアント側でのデータ保持APIなどがある
    • サードパーティAPI : サードパーティのプラットフォーム(TwitterやFacebook)上に作られた構造で、それらの機能をWebページで利用できるようにする(ex. Twitter API, Google Maps API, YouTube API)

クロージャ

  • 関数とその関数が作られた環境が一体となった特殊なオブジェクトのこと
  • あるコードブロック内で定義された関数などが、そのブロックをスコープとする変数などを参照できる
  • オブジェクト内部で使用している変数やメソッドを他のプログラムから容易に変更できないようになる(カプセル化)
  • ex. 関数createStopwatchのスコープ内で定義された変数timeと関数の結果が一体となったオブジェクトを変数stopwatchへ代入しているため、変数stopwatchが呼び出される都度、変数timeは0に初期化されることなく、下記のような結果が返ってくる
var createStopwatch = function() {
    var time = 0;
    return function() {
        time += 1;
        console.log(time);
    };
};
var stopwatch = createStopwatch();
stopwatch();    // 1が返ってくる
stopwatch();    // 2が返ってくる
stopwatch();    // 3が返ってくる

4. Node.jsの基本知識

Hello, Nodeを出力

hello.js
console.log('Hello, Node')
  • プログラムを実行
$ node hello.js
  • 実行結果
Hello, Node
  • 'use strict';を宣言するとstrict(厳格)モードで実行できる
    • strictモード : javascriptのコードをより厳しくエラーチェックすることができる仕組み
hello.js
'use strict';    
console.log('Hello, Node')
  • Webサーバとして動作させる場合
hello2.js
var http = require("http");    // HTTPモジュールの読み込み

http.createServer(function(request, response) {    //  HTTPサーバを作成
   response.writeHead(200, {'Content-Type': 'text/plain'});    // レスポンスHTTPヘッダーを設定
   response.end('Hello, Node\n');    // レスポンスボディを送信
}).listen(8000);    // ポート8000でリクエストを行う

//  サーバにアクセスするためのURLを出力 
console.log('Server running at http://127.0.0.1:8000/');
  • プログラムを実行
$ node hello.js    // ブラウザで"http://localhost:8000"にアクセス Hello, Nodeと表示される

非同期処理

  • 処理を実行したら結果を待たずに他の処理を実行できる(複数の処理を平行して実行できる)
    • 同期処理は、上から下へ1行ずつ順番にプログラムが実行されていく(サーバーへアクセスをして値を取得する間プログラムはストップしている)
// 操作が完了する前に次の処理を実行する
setTime(function() {
   console.log('First');
   }, 3000);    // 処理に3秒間かかる
console.log('Second');
  • 実行結果
Second
First
  • Node.js ではPromiseという仕組みを使って非同期を実現
    • Promiseとは非同期処理を実現するために用意されたオブジェクト
    • 非同期処理の成功時(resolve)、失敗時(reject)の処理を明示的に書くことが出来る
var promise = new Promise(function(resolve, reject) {    // Promiseオブジェクトを変数に代入
  setTimeout(function() {
    resolve('hoge');    // 処理成功時にresolveが呼ばれる 引数に返したい結果となる値を指定
  }, 3000);
});

promise.then(function(value) {    // then()の中の関数の引数valueにPromiseの結果が格納されいる
  console.log(value);    // 3秒待ってhogeが返されることが約束されている
});

console.log(promise);    // [object Promise]が返される

実行結果

[object Promise]
hoge

5. ライブラリの把握

  • axios
    • HTTP通信を簡単に行うことができるJavascriptのライブラリ
  • request
    • 標準のhttpライブラリを使うより簡単で理解しやすい記述でHTTP通信を行うことができるライブラリ
  • Moment.js
    • JavaScriptで日付を扱うためのライブラリ
    • 日時の加算減算や2つの日付の差の計算などができる
  • facebook/jest
    • JavaScriptのテストフレームワーク

6. 今後の課題

  • npm(Node.jsのパッケージを管理するもの)を学習する際に、Node.jsについての理解を深める

参照

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

Slack の Bolt フレームワークのチュートリアルを Heroku 上で実行する

最近 Slack のイベントにも参加したりした時に色々と聞いたので、自分でもやってみたものです。
ほとんど公式ドキュメントをなぞったものなので、難しい内容とかは特にないかと思います。
シンプルに 「Heroku で開発するならどうやるのか」 を試した感じです。

前提条件

これからやる作業の前提条件として、次のことはすでに済ませてある前提で進めていきます。

  1. Heroku のアカウントを作成済み
  2. Heroku CLI をインストール・設定済み(ログインとか)
  3. Node.js をインストール済み
  4. Git をインストール・設定済み

エディタはお好みのものをご利用ください。

これからやること

  1. Slack の Bolt フレームワークにあるアプリを作成
  2. 作成したアプリを Heroku 上にアップする
  3. 余力があればカスタマイズ

手順

アップロード先の Heroku アプリの準備

Slack の App を作る前に、次のコマンドであらかじめアップロード先の Heroku アプリを作成しておきます。

$ heroku apps:create <アプリの名前を半角英数字で>
Creating ⬢ <アプリ名>... done
https://<アプリ名>.herokuapp.com/ | https://git.heroku.com/<アプリ名>.git

heroku apps:create の後に何も入力せずに実行すると、Heroku が自動でランダムな名前を生成して設定しますが、分かりやすいように名前を指定しておくことをお勧めします。

アプリの作成が終わると、Web の URL と Git の URL の2つが表示されますので、どちらも控えておきます。
URL はこんな感じです。

Web の URL

https://<アプリ名>.herokuapp.com/

Git の URL

https://git.heroku.com/<アプリ名>.git

控え損ねた場合は、heroku apps:info <アプリ名> で確認もできます。(ブラウザからダッシュボードでもいけるはず)

Slack App の作成

今度は Slack 側のアプリの作成、設定をしていきます。

  1. Slack API のサイトの Your Appsの画面から Create New App ボタンをクリックします。
    New_App_Create_Screen.png

  2. ポップアップでアプリ名とインストール先のワークスペースを聞かれるので、必要事項を入力して Create App ボタンをクリックします。

  3. アプリが作成されると Basic Information の 画面に遷移します。このページの App Credential の欄には、後ほど使用する認証情報が記載されています。

  4. 左側のメニューから Bot Users をクリックします

  5. Add A Bot User をクリックして、表示名とユーザ名を設定し、Add Bot User をクリックします

  6. 左側のメニューから Install App をクリックします

  7. Install App to Workspace をクリックしてワークスペースにインストールします

  8. OAuth トークンが2種類生成されます
    OAuth_Token_Info.png

この後で Bot User OAuth Access Token の方を使っていきます。

Slack App の開発

ここからは実際にサンプルの Slack App を作っていきます。
なお、Heroku アプリへのデプロイには Git を使用するため、途中で Git コマンドも使用します。

  1. プロジェクトディレクトリを作成し、中に移動します(ディレクトリの名前は任意)

    $ mkdir <プロジェクトディレクトリ名>
    $ cd <プロジェクトディレクトリ名>
    
  2. git init コマンドで Git の管理対象に設定します

    $ git init
    
  3. .gitignore ファイルを作成します(中身は Heroku の公式サンプルを参考にしました。)

    $ vi .gitignore
    
    .gitignore
    # Node build artifacts
    node_modules
    npm-debug.log
    
    # Local development
    *.env
    *.dev
    .DS_Store
    
    # Docker
    Dockerfile
    docker-compose.yml
    
  4. Procfile を作成します。Procfile は起動時にアプリが実行するコマンドを記載するファイルです。ここでは node app.js コマンドを記載します。

    $ vi Procfile
    
    Procfile
    web: node app.js
    
  5. npm init コマンドでプロジェクトの設定を行います(私はこんな感じで実行しました)

    $ npm init
    This utility will walk you through creating a package.json file.
    It only covers the most common items, and tries to guess sensible defaults.
    
    See `npm help json` for definitive documentation on these fields
    and exactly what they do.
    
    Use `npm install <pkg>` afterwards to install a package and
    save it as a dependency in the package.json file.
    
    Press ^C at any time to quit.
    package name: (<プロジェクトディレクトリ名>) 
    version: (1.0.0) 
    description: 
    entry point: (index.js) app.js
    test command: 
    git repository: 
    keywords: 
    author: 
    license: (ISC) 
    About to write to <プロジェクトディレクトリの親のパス>/bolttest/package.json:
    
    {
      "name": "bolttest",
      "version": "1.0.0",
      "description": "",
      "main": "app.js",
      "scripts": {
         "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }
    
    Is this OK? (yes) 
    

    出来上がった package.json ファイル

    package.json
    {
      "name": "bolttest",
      "version": "1.0.0",
      "description": "",
      "main": "app.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }
    
  6. Bolt パッケージをインストールします

    $ npm install @slack/bolt
    
  7. app.js 内に次のコードを記述します。Heroku の場合、ポート番号は動的に割り振られるため、process.env.PORT の書き方で良いようです。

    $ vi app.js
    
    app.js
    const { App } = require('@slack/bolt');
    
    const app = new App({
      token: process.env.SLACK_BOT_TOKEN,
      signingSecret: process.env.SLACK_SIGNING_SECRET
    });
    
    (async () => {
      // Start your app
      await app.start(process.env.PORT || 3000);
    
      console.log('⚡️ Bolt app is running!');
    })();
    
  8. git でローカルにコミットします

    $ git add .
    $ git commit -m "First Commit"
    
  9. ローカルリポジトリのリモートの設定を追加します。Heroku アプリへのデプロイは、ローカルのリポジトリを Heroku のリポジトリにそのままプッシュすることなので、あらかじめ登録しておきます。URL は アップロード先の Heroku アプリの準備 の手順ででた Git の URL です。

    $ git remote add heroku <Git の URL>
    
  10. heroku リポジトリにプッシュします

    $ git push heroku master
    
  11. トークンを設定します。Heroku では config vars という仕組みがあるので、それを使用します

    $ heroku config:set SLACK_SIGNING_SECRET=<サインインシークレット>
    $ heroku config:set SLACK_BOT_TOKEN=xoxb-<Bot トークン>
    
  12. インスタンスを heroku コマンドで立ち上げます

    $ heroku ps:scale web=1
    
  13. ログを確認します。ログの確認には heroku logs コマンドを使用します

    $ heroku logs -t
    

    次のログが出ているかと思います。

     ⚡️ Bolt app is running!
    
  14. Slack api の左側のメニューから Event Subscription をクリックして、イベント URL を登録し Save Change をクリックします。設定する URL は Heroku の Web の URL の末尾に /slack/events をつけたものとなります。

    https://bolttestbot.herokuapp.com/slack/events
    

    こんな感じになれば OK です。
    Event_Subscription.png

  15. 現状だと何も反応しないため、app.message の処理を追加します。

    $ vi app.js
    
    app.js
    const { App } = require('@slack/bolt');
    
    const app = new App({
      token: process.env.SLACK_BOT_TOKEN,
      signingSecret: process.env.SLACK_SIGNING_SECRET
    });
    
    // Listens to incoming messages that contain "hello"
    app.message('hello', ({ message, say }) => {
      // say() sends a message to the channel where the event was triggered
      say(`Hey there <@${message.user}>!`);
    });
    
    (async () => {
      // Start your app
      await app.start(process.env.PORT || 3000);
    
      console.log('⚡️ Bolt app is running!');
    })();
    
  16. ローカルにコミットします

    $ git add app.js
    $ git commit -m "Add hello message"
    
  17. Heroku にプッシュします

    $ git push heroku master
    

これで チャンネルに Bot ユーザを招待して、hello を含むメッセージを投稿すると反応してくれます。

Bot_Response.png

メッセージのカスタマイズ

メッセージがテキストのみなので、ボタンを追加するチュートリアルにしたがってやっていきます。

  1. Slack api の画面左側の Interactive Components をクリックして、Request URL を登録し、Save Change をクリックします。登録する URL は Event Subscription のところで登録したものと同じものを登録します。

    Interactive_Components.png

  2. ボタン付きメッセージを返すようにコードを修正します

    $ vi app.js
    
    app.js
    const { App } = require('@slack/bolt');
    
    const app = new App({
      token: process.env.SLACK_BOT_TOKEN,
      signingSecret: process.env.SLACK_SIGNING_SECRET
    });
    
    // Listens to incoming messages that contain "hello"
    app.message('hello', ({ message, say }) => {
      // say() sends a message to the channel where the event was triggered
      say({
        blocks: [
        {
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": `Hey there <@${message.user}>!`
          },
          "accessory": {
            "type": "button",
            "text": {
              "type": "plain_text",
              "text": "Click Me"
            },
            "action_id": "button_click"
          }
        }
        ]
      });
    });
    
    (async () => {
      // Start your app
      await app.start(process.env.PORT || 3000);
    
      console.log('⚡️ Bolt app is running!');
    })();
    
  3. ローカルにコミットします

    $ git add app.js
    $ git commit -m "change message to block"
    
  4. Heroku にプッシュします

    $ git push heroku master
    

    これで hello を含むメッセージを投稿した時にボタン付きのメッセージがボットから返されますが、ボタンをクリックした時の処理がないので何も起こりません。(正確には、ボタンを押すと右側にビックリマークが出ます)

  5. ボタンクリック時のアクションを app.action で追加します

    $ vi app.js
    
    app.js
    const { App } = require('@slack/bolt');
    
    const app = new App({
      token: process.env.SLACK_BOT_TOKEN,
      signingSecret: process.env.SLACK_SIGNING_SECRET
    });
    
    // Listens to incoming messages that contain "hello"
    app.message('hello', ({ message, say }) => {
      // say() sends a message to the channel where the event was triggered
      say({
        blocks: [
        {
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": `Hey there <@${message.user}>!`
          },
          "accessory": {
            "type": "button",
            "text": {
              "type": "plain_text",
              "text": "Click Me"
            },
            "action_id": "button_click"
          }
        }
        ]
      });
    });
    
    app.action('button_click', ({ body, ack, say }) => {
      // Acknowledge the action
      ack();
      say(`<@${body.user.id}> clicked the button`);
    });
    
    (async () => {
      // Start your app
      await app.start(process.env.PORT || 3000);
    
      console.log('⚡️ Bolt app is running!');
    })();
    
  6. ローカルにコミットします

    $ git add app.js
    $ git commit -m "Add button click action"
    
  7. Heroku にプッシュします

    $ git push heroku master
    

これでボタンをクリックした時にメッセージが投稿されるようになりました。

Button_Clicked.png

私が実施したソースコードは GitHub のリポジトリにあります。
silverskyvicto/bolttest

余談

Slack の瀬良さん @seratch がサンプルのものをすでに作成していて、それを Heroku Button として公開していることを後で知りました。

Slack Bolt app on Heroku

参考

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

ESLint v6.7.0

v6.6.0 | 次 (2019/12/21 JST)

ESLint 6.7.0 がリリースされました。
小さな機能追加とバグ修正が行われています。

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット
? 本家リポジトリ
? 本家サポート チャット

? 本体への機能追加

設定ファイルのignorePatternsプロパティ

? RFC022, #12274

共有設定を含む設定ファイルで、ignorePatternsプロパティが利用できるようになりました。このプロパティは.eslintignoreと同様に ESLint が無視するファイルをコントロールできます。

例(.eslintrc.yml)
ignorePatterns:
- "!.eslintrc.js"
- "/node_modules/"
- "**/node_modules/"

extends:
- eslint:recommended

rules:
  quotes: error
  #...

Suggestions API

? RFC030, #12384

あるエラーが複数の修正候補を持つ場合、ESLint は自動修正を行えず、修正候補を提示する手段もありませんでした。新しい Suggestions API を使うと、エディタの ESLint プラグイン等のインタラクティブな UI を通して修正候補を提示できるようになります。

今のところ、ユーザーはまだこの機能を利用できません。各検証ルールとエディタ プラグインのサポートをお待ちください。

【非推奨】個人設定ファイル

? RFC032, #12426

ESLint が実行されたディレクトリに設定ファイル (.eslintrc.*) が存在しなかった場合、ESLint は OS のホーム ディレクトリにある設定ファイルを読み込みます。この機能を個人設定ファイルと呼びます。

この機能が非推奨になりました。

  • ESLint 7 以降、この機能を利用すると実行時警告が出力されるようになります。
  • ESLint 8 でこの機能は削除されます。

今後もホームディレクトリの設定ファイルを利用したい場合は、CLI オプションで明示的に指定してください。

eslint --config ~/.eslintrc.json lib

【非推奨】sourceCode#isSpaceBetweenTokens()

? #12519

sourceCode#isSpaceBetweenTokens()メソッドが名前変更されました。新しい名前はsourceCode#isSpaceBetween()になります。

トークンだけでなく AST ノードを渡しても動作するので、単純に名前が間違っていたという理由です。元の名前のメソッドは非推奨メソッドとして残され、将来のメジャーリリースで削除されます (具体的な削除プランは示されていません)。

? 新しいルール

grouped-accessor-pairs

? #12331

Getter と Setter のペアを離れた場所に定義すると警告するルールです。

/* eslint grouped-accessor-pairs: error */

//✘ BAD
const obj1 = {
   get value() { },
   foo() { },
   set value(v) { },
}

//✔ GOOD
const obj2 = {
   get value() { },
   set value(v) { },
   foo() { },
}

Online Playground

no-setter-return

? #12346

Setter に値を返すreturn文を書くと警告するルールです。getter-return ルールの兄弟です。

/* eslint no-setter-return: error */

//✘ BAD
const obj1 = {
    set value(v) {
        return this._value
    },
}

//✔ GOOD
const obj2 = {
    set value(v) {
        if (v == null) return
        this._value = v
    },
}

Online Playground

prefer-exponentiation-operator

? #12360

Math.pow()関数の代わりに**演算子を使うように指示するルールです。

/* eslint prefer-exponentiation-operator: error */

//✘ BAD
const b1 = Math.pow(2, 8);
const b2 = Math.pow(a, b);
const b3 = Math.pow(a + b, c + d);
const b4 = Math.pow(-1, n);

//✔ GOOD
const a1 = 2 ** 8;
const a2 = a ** b;
const a3 = (a + b) ** (c + d);
const a4 = (-1) ** n;

Online Playground

no-dupe-else-if

? #12504

else-if文の連鎖の中で、重複する条件式のために常にfalseになるif文を警告するルールです。

/* eslint no-dupe-else-if: error */

//✘ BAD
if (a || b) {
    //...
} else if (a) {
    //...
}

//✔ GOOD
if (a || b) {
    //...
} else if (c) {
    //...
}

Online Playground

no-constructor-return

? #12529

コンストラクタの中でreturn文で値を返すことを禁止するルールです。

/* eslint no-constructor-return: error */

//✘ BAD
class B {
    constructor() {
        return {}
    }
}

//✔ GOOD
class A {
    constructor(arg) {
        if (arg == null) return
        this.value = arg
    }
}

Online Playground

? オプションが追加されたルール

no-underscore-dangle allowAfterThisConstructor

? #11489

this.constructor に続くアンダーバーを許可するオプションが追加されました。

/* eslint no-underscore-dangle: [error, { allowAfterThisConstructor: true }] */

//✘ BAD
class B {
    foo(obj) {
        obj.constructor._privateStuff
    }
}

//✔ GOOD
class A {
    foo(obj) {
        this.constructor._privateStuff
    }
}

Online Playground

no-implicit-globals lexicalBindings

? #11996

レキシカル スコープの宣言 class, const, let も警告するルールが追加されました。

/* eslint no-implicit-globals: [error, { lexicalBindings: true }] */

//✘ BAD
class B {}
const b1 = 0
let b2 = 0

//✔ GOOD
{
    class A {}
    const a1 = 0
    let a2 = 0
}

Online Playground

no-useless-computed-key enforceForClassMembers

? #12110

クラス構文の不要な Computed Keys も警告するオプションが追加されました。

/* eslint no-useless-computed-key: [error, { enforceForClassMembers: true }] */

//✘ BAD
class B {
    ["foo"]() { }
}

//✔ GOOD
class A {
    [foo]() { }
}

Online Playground

no-invalid-this capIsConstructor

? #12308

ES5 スタイルのコンストラクタ (名前が大文字で始まる関数) をコンストラクタとして扱わないオプションが追加されました。React では関数コンポーネントに大文字で始まる関数名を使う習慣があるためです。

/* eslint no-invalid-this: [error, { capIsConstructor: false }] */

//✘ BAD
function Foo() {
    this.value = 0
}

Online Playground

✒️ eslint --fix をサポートしたルール

特になし。

⚠️ 非推奨になったルール

特になし。

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

Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる

Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる

この記事に書いてあること

  • Expressの実装例
  • Vue.jsの実装例
  • 実行環境として利用するDockerコンテナの作り方
  • Sequelizeの導入方法、簡単な使い方
  • 拙い日本語

しがないエンジニアが人生初アドベントカレンダー参加となりますので、諸々ご容赦頂けますと幸いです。

対象者

細かい説明は割愛していますが、初心者向けに書いてます。

「ゴリゴリにDocker使った環境構築が知りたい!」とか、
「俺はSequelizeなんか使わずにSQLを一つずつ組むね!」とか、
「ここどうなってるかもうちょい細かく説明してほしい」みたいな人にはあまり適してません。

あくまで、
記事をなぞっていくだけで手軽にCRUDがVue.js+Expressが体験できる

っていうのを目的にしています。

各種バージョン

Docker for mac 19.03.5
Node.js 12.13.0
express 4.16.1
Sequelize-cli 5.5.1
vue-cli 4.0.5

DBはsqlite3を利用(なんでもいいけど)

作るもの

CRUD機能を搭載した簡単なTodoリストを作ります。
完成イメージはこんな感じです。

cwljp-8g071.gif

各種説明

Docker

言わずと知れたコンテナマン。コンテナの概念やメリットの説明は割愛。今回環境はこれで構築。

(ベストプラクティスを知りたい)

Vue.js

みんな大好きフロントフレームワーク。特に難しいことはしません。
(vue-cliを使って雛形を作成)

Node.js

みんな大好きサーバーサイドで動くJavaScript。特に難しいことはしません。

Sequelize

Nodeで使えるORM。RailsのActiveRecordみたいな物だと思ってもらえればOK。
特に難しいことはしません

全体的なファイル構成

ファイル構成はこんな感じ。
rootディレクトリ配下をコンテナごとに区切り、後々コンテナにマウントしてあげる。

rootDir/
 ┣ docker-compose.yml
 ┣ vue/
 ┃  ┣ Dockerfile
 ┃  ┗ frontapp/
 ┃      ┗ Vue.jsの雛形ファイル群が入ってくる
 ┗ node/
    ┣ Dockerfile
    ┗ Expressの雛形ファイル群が入ってくる

全体構成

全体構成はこんな感じです。
基本Vue.jsコンテナがリクエストを受けて、axiosでNode.jsコンテナにサーバ通信しています。
SQLiteを使っているので、DBアクセスはNode.jsコンテナ内で処理されるようなイメージ。

vuexpress.png

Node.jsの準備

早速開始、と行きたいところですが、まずは実行環境を構築。
Dockerhubから引っ張ってきてもいいのですが、せっかくなのでDockerfileを書いてあげましょう。
今回はとりあえず各種プログラムが実行できる環境が前提なので、コンテナ内は環境のみ。
ソースファイルはコンテナイメージに含めず、docker-composeを使って後でマウントしてあげることにします。

node/Dockerfile
FROM node:12.13
RUN npm install -g express-generator sequelize-cli

Dockerfileの記述が終わったら一旦、Dockerfileからコンテナイメージを作成。
コンテナ起動時にローカルのディレクトリをマウントし、コンテナ内のデータが永続化できるようにします。

docker build node/. -t serverapp:latest
docker run -itd --rm --name serverapp -v $PWD/node:/node serverapp:latest

コンテナの起動が完了したら、コンテナ内にログインする。

docker exec -it serverapp /bin/bash

コンテナログイン後、expressコマンドを実行し、雛形ファイル群を作成。

cd /node
express .
npm install --save sequelize sqlite3 cors nodemon
npm install

docker runを実行した際にローカルのフォルダをマウントしているので、
ローカルのnode/以下にexpressの雛形ファイル群ができているはず。

続いて、DBを作成するためにコンテナに入ったままsequelize initを実行し、
CRUDに必要なtaskモデルを作成する準備をします。

sequelize init

init実行後、一旦コンテナからログアウトし、ローカルでconfig/config.jsonを下記のように修正。

config/config.json
{
  "development": {
    "username": "root",
    "password": null,
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "sqlite",
    "storage": "./data/development.sqlite3",
    "operatorsAliases": false
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "sqlite",
    "storage": "./data/test.sqlite3",
    "operatorsAliases": false
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "sqlite",
    "storage": "./data/production.sqlite3",
    "operatorsAliases": false
  }
}

Sequelizeのデータが保存される./dataを作成する。

mkdir data

もう一度コンテナにログインし、taskモデルを作成します。

docker exec -it serverapp /bin/bash
sequelize model:create --name task --underscored --attributes taskname:string
sequelize db:migrate

マイグレーションが無事に成功すればtaskモデルが作成されます。
これでNode.jsの準備は完了。雛形準備に利用したコンテナは停止します。

docker stop serverapp

続いてフロントVue.js側を準備します。

Vue.jsの準備

Node側と同様にDockerfileを記述。

vue/Dockerfile
FROM node:12.13
RUN npm install -g @vue/cli

Dockerfileを元にコンテナイメージを作成。起動し、ローカルのフォルダをマウント。

docker build vue/. -t frontapp:latest
docker run -itd --rm --name frontapp -v $PWD/vue:/vue frontapp:latest

Node側同様、Vue.jsの雛形ファイル群を作成するため、一度コンテナ内にログインします。

docker exec -it frontapp /bin/bash

コンテナログイン後、下記コマンドを実行。オプションは default で問題ありません。
その後の選択肢としては、yarnnpmが出てくるけど、個人的にはnpmのが使いやすいのでそちらで。

cd /vue
vue create frontapp

以上でVue.js側の準備は終了。Node.js側同様に、一度コンテナは停止します。

docker stop frontapp

docker-compose.ymlの準備

Node.jsとVue.jsそれぞれのコンテナを起動する際、composeファイルがあると起動/終了が楽なので、
docker-compose.ymlを下記のように記入。

docker-compose.yml
version: "3"
services:
  node:
    build: node/.
    volumes:
      - ./node:/node
    working_dir: /node
    command: ["npm", "start"]
    ports:
      - "3000:3000"
  vue:
    build: vue/.
    volumes:
      - ./vue:/vue
    working_dir: /vue/frontapp
    command: ["npm", "run", "serve"]
    ports:
      - "8080:8080"

プロジェクトのカレントディレクトリで、docker-composeコマンドを実行し、
Node.jsのコンテナとVue.jsのコンテナを起動する。

docker-compose up -d
# コンテナ終了は docker-compose down

ブラウザで3000ポートにアクセスしExpressの画面、8080ポートにアクセスしVue.jsの画面が表示されれば、開発用コンテナの構築は完了です。

localhost:3000

スクリーンショット 2019-11-23 14.15.48.png

localhost:8080

スクリーンショット 2019-11-23 14.23.14.png

docker-composeコマンドで初回起動時にコンテナイメージが再ビルドされ、 docker run で起動した時のコンテナイメージとは別の名前でイメージ化されるため、 docker run コマンドで起動した時のコンテナイメージは不要のため削除します。

docker rmi serverapp:latest
docker rmi frontapp:latest

実行環境の作成はこれにて終了。次から各種機能の実装に入っていきます。

Node.js

まずはサーバサイドの処理から実装します。
処理実装に入る前に、ソースコード変更時に自動的にNodeが再起動されるように少しだけ工夫。
app.jsdocker-compose.ymlを修正します。

node/app.js
...
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
var cors = require("cors");

var app = express();

app.use(cors());
...
app.listen(3000, function() {
  console.log("Node server is started");
});

module.exports = app
docker-compose.yml
...
    working_dir: /node
    command: ["./node_modules/.bin/nodemon", "app"]
    ports:
...

Node.jsの初期状態だとVueからのリクエストを受けられない(エラーがでてしまう)ので、
インストールしたcorsモジュールを追加して、corsを許可する必要があります。
また、nodemonを利用しファイル変更を検知、ファイルが変更されたら自動でサーバが再起動されるようにします。
設定を反映するため、一度コンテナ再起動を実行。

docker-compose down
docker-compose up -d

コントローラーの新規追加がめんどくさいので最初から用意されているindex.jsを利用します。
実装内容としてはこんな感じ。

node/routes/index.js
var express = require("express");
var router = express.Router();
const db = require("../models/index");

// Read
router.get("/", async function(req, res, next) {
  try {
    const result = await db.task.findAll({});
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

//Create
router.post("/task", async function(req, res, next) {
  try {
    const result = await db.task.create({
      taskname: req.body.task
    });
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

//Update
router.put("/task/:id", async function(req, res, next) {
  try {
    const result = await db.task.update(
      {
        taskname: req.body.task
      },
      {
        where: {
          id: req.params.id
        }
      }
    );
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

//Delete
router.delete("/task/:id", async function(req, res, next) {
  try {
    const result = await db.task.destroy({
      where: {
        id: req.params.id
      }
    });
    res.send({
      result: result
    });
  } catch (err) {
    res.status(500).send(err);
  }
});

module.exports = router;

送られてきたパラメータをそのままDBに登録するっていう簡単なCRUD処理の一覧です。
一つずつ説明していきます。

// Read
router.get("/", async function(req, res, next) {
  try {
    const result = await db.task.findAll({});
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

getで/にアクセスされた際に、taskテーブルから全てデータを引っ張ってくるように実装。
DBのtaskテーブルから全てデータを引っ張ってくるのに、sequelizeのfindAllというメソッドを使用します。
取得したデータをres.sendを使用して、リクエスト元に戻してあげます。

router.post("/task", async function(req, res, next) {
  try {
    const result = await db.task.create({
      taskname: req.body.task
    });
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

postで/taskにアクセスした際に、リクエストのbody内容をDBのtaskテーブルに登録。
sequelizeのcreateというメソッドを利用します。

router.put("/task/:id", async function(req, res, next) {
  try {
    const result = await db.task.update(
      {
        taskname: req.body.task
      },
      {
        where: {
          id: req.params.id
        }
      }
    );
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

putで/task/:idにアクセスした際に、idに紐づくレコードをupdateする処理。
sequelizeのupdateメソッドを利用し、リクエストのbody内容でデータを更新します。

router.delete("/task/:id", async function(req, res, next) {
  try {
    const result = await db.task.destroy({
      where: {
        id: req.params.id
      }
    });
    res.send({
      result: result
    });
  } catch (err) {
    res.status(500).send(err);
  }
});

deleteで/task/:idにアクセスした際に、idに紐づくレコードを削除する処理。
sequelizeのdestroyメソッドを利用。

Node.js側CRUDの処理は実装完了です。続いて、Vue.js側の実装。

Vue.js

デフォルトでHelloWorld.vueというコンポーネントが存在しているので、そこに肉付けしていくことにします。
まずは各種部品を設置。

vue/frontapp/src/components/HelloWorld.vue
<template>
  <div class="hello">
    <form>
      <input type="text" style="display:none" />
      <input type="text" />
      <input type="button" value="add!" />
    </form>
    <table align="center" border="0">
      <tr>
        <th>task</th>
        <th>update</th>
        <th>delete</th>
      </tr>
      <tr>
        <td>
          <input type="text" />
        </td>
        <td>
          <input type="button" value="update" />
        </td>
        <td>
          <input type="button" value="delete" />
        </td>
      </tr>
    </table>
  </div>
</template>
<script>
export default {
  name: "HelloWorld"
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
.table {
  height: 100%;
  text-align: center;
}
</style>

上の基本系を修正します。今の時点だとこんな感じ。
スクリーンショット 2019-11-23 10.12.44.png

まずはGETで/にアクセスした際に、apiアクセスを行う部分を実装する。

vue/frontapp/src/components/HelloWorld.vue
<script>
import axios from "axios";
export default {
  name: "HelloWorld",
  data: () => ({
    tasks: [],
  }),
  created: async function() {
    try {
      const result = await axios.get("http://localhost:3000");
      this.tasks = result.data;
    } catch (err) {
      alert(JSON.stringify(err));
    }
  }
}
...

createdを定義しておくことで、ページが読み込まれた時に処理を実行することが可能。
createdでもmountedでもどっちでもいいけど、今回の用途にはcreatedの方が適任ですね。
下記記事で詳しく書かれていたので、気になる人は参照してみてください。

Vuejs APIアクセスはcreatedとmountedのどちらで行う?

axiosは非同期で実行されるので、処理を同期的に行うためにawaitで実行。
実行結果としてresult.dataが戻ってくるので、data内のtasksの内容をAPI実行結果に変更する。

続いてtask追加処理を実装。まず入力されたデータにアクセスできるようにする必要があるため、
テキストボックスをv-modelでdataと紐づけます。

vue/frontapp/src/components/HelloWorld.vue
...
    <form>
      <input type="text" style="display:none" />
      <input v-model="currentTask" type="text" />
      <input type="button" value="add!" />
    </form>
...
<script>
import axios from "axios";
export default {
  name: "HelloWorld",
  data: () => ({
    tasks: [],
    currentTask: ""
  }),
...

こうすることでinputに入力されたデータが、datacurrentTaskに反映され、関数内からデータが参照可能となります。
次に@clickイベントを実装し、ボタンが押された時に関数を呼び出すよう修正。

vue/frontapp/src/components/HelloWorld.vue
...
    <form>
      <input type="text" style="display:none" />
      <input v-model="currentTask" type="text" />
      <input type="button" value="add!" @click="taskCreate" />
    </form>
...
<script>
...
  created: async function() {
    try {
      const result = await axios.get("http://localhost:3000");
      this.tasks = result.data;
    } catch (err) {
      alert(JSON.stringify(err));
    }
  },
  methods: {
    taskCreate: async function() {
      alert(this.currentTask);
    }
  }
};
</script>

こうすることで、ボタンが押された時に、taskCreateを呼び出すことが可能です。
仮実装としてボタンを押すと、テキストボックス内に入力したデータの内容をalert表示するように実装してます。
何かしらテキストボックスに入力し、ボタンを押したタイミングでalertが表示されてくればOK。

スクリーンショット 2019-11-23 10.27.08.png

取得したデータをサーバに送信するため、taskCreate関数の続きを実装していく。

vue/frontapp/src/components/HelloWorld.vue
<script>
...
methods: {
    taskCreate: async function() {
      try {
        const result = await axios.post("http://localhost:3000/task", {
          task: this.currentTask
        });
        this.tasks.push(result.data);
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    }
...

node.js側で定義したタスク追加の処理(/task)に繋げる。
サーバ処理終了後に下記部分で動的にtasksにデータを追加。

this.tasks.push(result.data);
// data内tasks配列に戻り値(追加したデータ)を追加
this.currentTask = "";
// data内currentTaskとテキストボックスが双方向でバインドされているので、currentTaskを空にすることでテキストボックスが空になる

これで、入力されたデータがサーバにリクエストで送られ、データ保存が可能となります。
このままだとデータを追加しても追加したデータが表示されないので、data内のtasks<tr>v-modelを利用し紐づけます。
HelloWorld.vueのtable部分を下記のように変更。

vue/frontapp/src/components/HelloWorld.vue
    <table align="center" border="0">
      <tr>
        <th>task</th>
        <th>update</th>
        <th>delete</th>
      </tr>
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input v-model="task.taskname" type="text" />
        </td>
        <td>
          <input type="button" value="update" />
        </td>
        <td>
          <input type="button" value="delete" />
        </td>
      </tr>
    </table>

v-forを利用することでtasksの数だけ <tr> が生成されます。
こうすることで、ページ読み込み時にサーバからデータが取得され、今まで追加したタスクが出てくるようになり、
ボタンを押した時にもタスクが画面に追加されるようになる。

スクリーンショット 2019-11-23 10.44.46.png

これでタスク追加に関しては実装終了。続いてタスク削除を実装します。
まずは関数を用意。

vue/frontapp/src/components/HelloWorld.vue
<script>
...
  methods: {
    taskCreate: async function() {
      try {
        const result = await axios.post("http://localhost:3000/task", {
          task: this.currentTask
        });
        this.tasks.push(result.data);
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskDelete: async function(id, index) {
      try {
        await axios.delete("http://localhost:3000/task/" + id);
        this.currentTask = "";
        this.tasks.splice(index, 1);
      } catch (err) {
        alert(JSON.stringify(err));
      }
    }
  }

taskDelete関数を追加しました。taskのidと配列のindexを引数に持ってあげます。
呼び出し部分はこんな感じ。

vue/frontapp/src/components/HelloWorld.vue
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input v-model="task.taskname" type="text" />
        </td>
        <td>
          <input type="button" value="update" />
        </td>
        <td>
          <input type="button" value="delete" @click="taskDelete(task.id, index)" />
        </td>
      </tr>

こうすることで、リクエスト先のURLが動的に作られます。
タスクのidが2だった場合は、リクエスト先はaxios.delete("http://localhost:3000/task/" + 2);となり、
タスクのidが10だった場合は、リクエスト先はaxios.delete("http://localhost:3000/task/" + 10);となります。

また、配列のindexを引数に持ってあげることによって、spliceメソッドを使って配列を操作することができ、画面のデータを非同期で変更可能です。

this.tasks.splice(index, 1);
// [{タスク1},{タスク2},{タスク3},{タスク4}]
// indexに2が渡された場合は、
// [{タスク1},{タスク2},{タスク4}]
// このように配列が操作される

最後にタスク修正した際の処理を実装する。まずは関数の用意から。

vue/frontapp/src/components/HelloWorld.vue
<script>
...
    taskUpdate: async function(id, val) {
      try {
        await axios.put("http://localhost:3000/task/" + id, {
          task: val
        });
        alert("タスクを修正しました");
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    }

呼び出し部分はこんな感じ。

vue/frontapp/src/components/HelloWorld.vue
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input v-model="task.taskname" type="text" />
        </td>
        <td>
          <input
            type="button"
            value="update"
            @click="taskUpdate(task.id, task.taskname)"
          />
        </td>
        <td>
          <input
            type="button"
            value="delete"
            @click="taskDelete(task.id, index)"
          />
        </td>
      </tr>

taskUpdateを呼び出す時に、タスクのID、タスク名を引数で渡してあげることにより、
対象のタスクのみアップデートがかかるようにします。

CRUDの全体が出来上がった最終形のHelloWorld.vueとしてはこんな感じ。

<template>
  <div class="hello">
    <form>
      <input type="text" style="display:none" />
      <input v-model="currentTask" type="text" />
      <input type="button" value="add!" @click="taskCreate" />
    </form>
    <table align="center" border="0">
      <tr>
        <th>task</th>
        <th>update</th>
        <th>delete</th>
      </tr>
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input v-model="task.taskname" type="text" />
        </td>
        <td>
          <input
            type="button"
            value="update"
            @click="taskUpdate(task.id, task.taskname)"
          />
        </td>
        <td>
          <input
            type="button"
            value="delete"
            @click="taskDelete(task.id, index)"
          />
        </td>
      </tr>
    </table>
  </div>
</template>
<script>
import axios from "axios";
export default {
  name: "HelloWorld",
  data: () => ({
    tasks: [],
    currentTask: ""
  }),
  created: async function() {
    try {
      const result = await axios.get("http://localhost:3000");
      this.tasks = result.data;
    } catch (err) {
      alert(JSON.stringify(err));
    }
  },
  methods: {
    taskCreate: async function() {
      try {
        const result = await axios.post("http://localhost:3000/task", {
          task: this.currentTask
        });
        this.tasks.push(result.data);
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskDelete: async function(id, index) {
      try {
        await axios.delete("http://localhost:3000/task/" + id);
        this.currentTask = "";
        this.tasks.splice(index, 1);
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskUpdate: async function(id, val) {
      try {
        await axios.put("http://localhost:3000/task/" + id, {
          task: val
        });
        alert("タスクを修正しました");
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
.table {
  height: 100%;
  text-align: center;
}
</style>

以上で完成です!

作ってみた感想

Vue.jsのバインディングが思っていた以上に使いやすく、非常に簡単にフロントの実装ができました。
規模が大きくなってきたらVuexとかライフサイクルとか考えないといけないこともあるけど、
この程度の規模のアプリケーションであれば、簡単に作ることができるのが良いですね。

あと、ただ実装するだけだと面白く無いので実行環境はDockerにしたのも◯。
汎用的な実行コンテナとして使えるので、チュートリアル的な使い方には非常に適任。
構築も簡単なので非常に使いやすかったかと。

Sequelizeの導入がちょっとだけめんどくさいけど、一度やってしまえばRailsチックにDB操作ができるので、
SQL書かずに手軽にDB操作したい!って人にはおすすめです。

明日は@kazukimatsumotoさんの番です。よろしくおねがいします!

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

Greengrass(v1.9.4)上でNode.js(v8.10)のLambdaのデプロイができない問題の解決法

※追記
Greengrass Nodejs SDKのGitHubに書いてますね。。
https://github.com/aws/aws-greengrass-core-sdk-js

Rename the file to nodejs8.10
Make sure the file is not a symlink.

シンボリックリンクもだめらしいです。


GreengrassにNode.js(v8.10)のLambdaをデプロイすると、エラーになります。

Deployment xxxxx of type NewDeployment for group xxxxx failed error: worker with xxxxx failed to initialize

ログを確認すると、nodejs8.10というバイナリがないよと言っているようです。

/greengrass/ggc/var/log/system/runtime.log
[ERROR]-runtime execution error: unable to start lambda container.  {"errorString": "failed to run container sandbox: container_linux.go:344: starting container process caused \"exec: \\\"nodejs8.10\\\": executable file not found in $PATH\""}

無理やり作ってやると、うまくいくようになりました。

sudo ln -s /usr/bin/nodejs /usr/bin/nodejs8.10

Node.jsのセットアップの問題?

環境

OS: Ubuntu 18.04(VirtualBox on Mac)
Greengrass: 1.9.4
Node.js:10.17.0

Node.jsのインストールは公式サイト( https://github.com/nodesource/distributions/blob/master/README.md )に従いました。(2019/11/24時点で最新のv10.17.0がインストールされました)

curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
sudo apt-get install -y nodejs

Raspberry Piでは続きがあります

Raspberry Piでも同様の問題が起きますが、上記手順でデプロイは成功するようになります。ただし、Lambdaの実行時にエラーとなって実行されません。
こちらは解決法がわかりません。。。ヘルプ。。。

Raspberry Pi Zero W
OS:Raspbian Buster
Greengrass: 1.9.3
Node.js:10.16.3

Lambdaが呼び出されると、以下のエラー

/greengrass/ggc/var/log/user/[リージョン]/[アカウントID]/[Lambda名].log
[ERROR]-standard_init_linux.go:207: exec user process caused "operation not permitted"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sequelizeパッケージを初心者が使ってみる。

sequelizeとは

Node.jsにはデータベースを利用するためのパッケージがたくさん存在します。
しかし、データベースを扱うにはSQLというデータベースを操作する言語が必要になります。
sequelizeというパッケージを使えば、SQLを知らなくても、Node.jsでデータベースを利用することができます。

さらに詳しく

sequlizeは公式によるとORM(Object-relational mapping, オブジェクト関係マッピング)と呼ばれるものらしいです。
以下は引用です。

オブジェクト指向の概念とリレーショナルデータベースの概念を橋渡しする役割がORMです。オブジェクト指向とリレーショナルデータベースの相性はそれぞれの概念が異なるため、相性が良くありません。なぜなら、オブジェクト指向では、データをオブジェクトとして扱うのですが、リレーショナルデータベースではデータを2次元の表として扱うためギャップが生じてしまいます。ORMを利用することによって、オブジェクトとデータベース問い合わせの相互変換を行います。
引用:ORMとは?

sequlizeはPostgreSQL、MySQL、MariaDB、SQLite、MicrosoftSQLServerに対応しているそうです。

sequlizeを利用してみる。

今回はsequlizeでpostgreSQLを利用してみます。

前提として以下を完了しておいてください。

① Node.jsのイントール
② Yarnのインストール
③ PostgreSQLのインストール 参考:https://lets.postgresql.jp/map/install

sequlizeを使う前にデータベースを作っておきます。
PostgreSQLのpsqlターミナルを起動し、以下のコマンドを入力してください。

$ create database rensyu;

データベース名は何でも構いません。
ここではrensyuとしておきます。

データベースが完成したら、sequelizeのインストールを行なっていきます。
以下のコマンドを入力してください。

$ yarn add sequelize
$ yarn add pg
$ yarn add pg-hstore

参照:sequelize入門

インストールが終わったら、
sequelizeを利用するためのJSファイルを準備し、
そのファイル内に以下を書き込みます。
今回はシンプルにIDと名前と年齢だけのデータベースを作っていきます。

post.js
'use strict';
const Sequelize = require('sequelize');
const sequelize = new Sequelize(
  'postgres://postgres:postgres@localhost/rensyu',
  {
    logging: false,
    define: {
      freezeTableName: true,
      timestamps: true
    }
  });
const Post = sequelize.define('Post', {
  id: {
    type: Sequelize.INTEGER,
    autoIncrement: true,
    primaryKey: true
  },
  name: {
    type: Sequelize.STRING
  },
  age: {
    type: Sequelize.INTEGER
  }
});

Post.sync();
module.exports = Post;

解説をしていきます。

const sequelize = new Sequelize(
  'postgres://postgres:postgres@localhost/rensyu',
  {
    logging: false,
    define: {
      freezeTableName: true,
      timestamps: true
    }
  });

これは、sequelizeのインスタンスを作成しています。
その際に、接続するデータベースを先ほど作成したものに設定しています。
postgresql://{ユーザー名}:{パスワード}@{ホスト名}/{データベース名}というようになっています。自分で作ったPostgreSQLのユーザー名やパスワードを利用してください。
loggingはデータベースに接続する際、コンソールログを行わない設定です。
defineのfreezeTableName: trueはテーブル名を固定する設定です。
timestampは自動的にcreatedAtという作成日時とupdatedAtという更新日時を自動的に追加してくれる設定です。

const Post = sequelize.define('Post', {
  id: {
    type: Sequelize.INTEGER,
    autoIncrement: true,
    primaryKey: true
  },
  name: {
    type: Sequelize.STRING
  },
  age: {
    type: Sequelize.INTEGER
  }
});

こちらはデータベースのテーブルやデータを定義し、Postというオブジェクトに代入しています。
'Post'はテーブル名です。
id, name, ageはテーブルの列です。
それぞれに数字であるのか、文字であるのかなどの設定がされています。
idのprimaryKey: trueというのは、データベースを操作する上で参照の対象となる主キーを、idに設定しているという意味です。

Post.sync();
module.exports = Post;

Post.sync();は、定義したPostというオブジェクトをデータベースに適用して同期を取っています。
module.exports = Post;は、オブジェクト自体をモジュールとして公開しています。

この他にもいろいろな設定オプションがあります。
https://sequelize.org/master/manual/getting-started.html

それでは、以上のデータベースを使ってみましょう。
適当なファイルを用意し、以下のような記述をします。

post-handler.js
const Post = require('./post');
Post.create({
    name: name,
    age: age
  });

const Post = require('./post');はモジュールをインストールしています。
Post.createでデータベースにデータを保存しています。
あとは、EJSやPugなどのテンプレートエンジンを使えば、nameとageに好きな値を入れて保存ができます。

以上です。

参考:sequelize APIリファレンス
参考:N予備校 プログラミングコース

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

Mocha, Chaiを使ったテストの表記パターン

TypeScriptでMocha, Chaiを使ったテスト駆動開発

インストール

TDDをサポートしてくれるパッケージのインストール。
$ npm install chai mocha ts-node @types/chai @types/mocha --save-dev 

参考: Testing TypeScript with Mocha and Chai

しかし、私の場合なぜかTypeScriptをグローバルインストールしているにも関わらず、テスト実行時に「typescriptモジュールが見つからない」とエラーが出てしまうので、ローカルに開発インストールを行いました。よってコマンドは以下になります。

テスト実行時のエラー例
✖ ERROR: Cannot find module 'typescript'

# typescriptが見つからない、とエラーが出る場合のインストール。
$ npm install typescript chai mocha ts-node @types/chai @types/mocha --save-dev 

package.json

(typescriptを含めた場合)最小限でこのようなpackage.jsonになるはず。
"scripts"下の"test"コマンド定義部分は"test"ディレクトリの下にあるファイル名が".ts"で終わるファイルを全て変更監視の対象にする場合の例です。対象のファイルが変更された場合には自動でtscによるコンパイルが行われ、テストが実行されます。

package.json
{
  "name": "testPatterns",
  "version": "1.0.0",
  "description": "samples for test cases.",
  "main": "index.js",
  "scripts": {
    "test": "mocha --require ts-node/register --watch-extensions ts \"test/**/*.ts\""
  },
  "author": "@olisheo",
  "license": "ISC",
  "dependencies": {},
  "devDependencies": {
    "@types/chai": "^4.2.5",
    "@types/mocha": "^5.2.7",
    "@types/node": "^12.12.12",
    "chai": "^4.2.0",
    "mocha": "^6.2.2",
    "ts-node": "^8.5.2",
    "typescript": "^3.7.2"
  }
}

前記した通り、上記は"typescript"がローカルインストールされている状態で、なぜかこれが必要でした。

シンプルなテストで動作確認

テストの実行コマンド
$ npm test -- -w

シンプルなテスト

とりあえず一番シンプルなテストで動作を確認する。パスするケースと、失敗するケースを一つづつ用意。

describe('simplest test:', () => {
  it('1 + 1 should be 2', () => {
    expect(1 + 1).to.equal(2);
  });
  it('the test should fail because it expects "1 + 1 = 0"', () => {
    expect(1 + 1).to.equal(0);
  });
});
実行と結果
$ npm test -- -w

> testPatterns@1.0.0 test /Users/user/project/testPatterns
> mocha --require ts-node/register --watch-extensions ts "test/**/*.ts" "-w"

  simplest test:
    ✓ 1 + 1 should be 2
    1) the test should fail because it expects "1 + 1 = 0"


  1 passing (18ms)
  1 failing

  1) simplest test:
       the test should fail because it expects "1 + 1 = 0":

      AssertionError: expected 2 to equal 0
      + expected - actual

      -2
      +0

想定通り、一つはパスして一つはフェイルしてます。

よく使うパターン

同期処理で例外が投げられたらパス

describe('Typical tests:', () => {
  it('immediate exception should synchronously be thrown.', () => {
    expect(()=>{
      // 想定通りならば例外が発生するケースを記述。例えば下のように例外が投げられればパスする。
      // throw new Error('just expected exception.');  
    }).to.throw();
  });
});

非同期処理をawaitで待つ

describe('Typical tests:', () => {
  it('using await, timer should successfully expires', async () => {
    const expirationMessage = await setTimer(1000);
    expect(expirationMessage).equals('OK!');
  });
});

// テスト対象の非同期関数。
function setTimer(msec: number): Promise<string> {
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      resolve('OK!');
    }, msec);
  });
}

Promiseを使った非同期。

Promiseをリターンで返すことで、Mochaが持っているPromiseのサポートを使える。しかし表記ミスを避けるために、できる限り前期のawaitを使った表記がいいと思う。ちなみにPromiseをリターンしないと、フェイルするテストがパスしてしまう。

describe('Typical tests:', () => {
  it('using promise, timer should always be rejcted after timeout.', () => {
    return setRejectionTimer(2000).then((expirationMessage)=>{
      expect.fail('test fails because the test case expects rejection.');
    }).catch((e)=>{
      expect(e).to.equal('NOT OK!');
    });
  });
});

// テスト対象の非同期関数。
function setRejectionTimer(msec: number): Promise<string> {
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      reject('NOT OK!');
    }, msec);
  });
}

///////////////// これはだめ! /////////////////////////////
describe('Typical tests:', () => {
  it('using promise, timer should always be rejcted after timeout.', () => {
    // 下は間違い。Primiseはリターンで返さないといけない。
    setRejectionTimer(2000).then((expirationMessage)=>{
      expect.fail('test fails because the test case expects rejection.');
    }).catch((e)=>{
      expect(e).to.equal('NOT OK!');
    });
  });
});

実は上記の動くバージョンでも本質的なテストにはなっていなくて、Promiseがrejectされなかった場合は、expect.fail('test fails because the test case expects rejection.') で例外を発生させているため、expect(e).to.equal('NOT OK!')の条件と合致してテストがパスしているのであって、expect.fail()を削除して例外を発生させてなければ、フェイルするべきテストもパスしてしまう。

非同期はto.throw()が使えなさそう。

以下も機能しない。

非同期関数内でrejectが発生してもテストはパスしない。
describe('Typical tests:', () => {
  it('delayed exception should asynchronously be thrown.', () => {
    expect(async ()=>{
      await setRejectionTimer(3000);
    }).to.throw(); // 機能しない。
  });
});

タイムアウトを回避したい場合のテスト実行コマンド

実行時間がかかるテストも多いので、タイムアウトを延ばすためのオプションはよく使います。

--timeoutパラメーターで30秒のタイムアウトを指示した場合。
$ npm test -- -w --timeout 30000

これから

「非同期処理の中で例外が起こること」を正確にアサートするのは、現状難しそうです。テスト対象にPromiseを扱いやすくするchai-as-promiseなるものがあるらしいので、時間を見つけて今度はそちらをかじってみたいと思います。

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