20210117のNode.jsに関する記事は13件です。

【Node.js Sequalize】 Unable to find migration: エラーの解決法

マイグレーションの誤操作により、マイグレートをかけた時に、削除したマイグレーションファイルが引っかかってきて、下記の様なエラーが表示された場合の解決法。

ERROR: Unable to find migration: 20210105140820-create-user.js

一旦,migrationsディレクトリの中に表示されたファイルを作成する。

20210105140820-create-user.js
'use strict';

module.exports = {
  up: function (queryInterface, Sequelize) {
    return Promise.resolve()
  },

  down: function (queryInterface) {
    return Promise.resolve()
  }
};

全てのマイグレーションをロールバックする。

npx sequelize-cli db:migrate:undo:all

先ほど作成したファイルを削除する。

マイグレートをかける。

npx sequelize-cli db:migrate --env development

以上

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

[Node.js] 2021年版サービス アカウント認証コード with ドメイン全体の委任

はじめに

2020年10月に G Suite が Google Workspace にリブランドされました。
それとは全く関係ないですが Google Classroom や Google Chat などの Google Apis を Node.js 用クライアントである googleapis を使って呼び出す際に、サービスアカウントに対してドメイン全体の委任を行い OAuth 認証を行う必要があったのですが、情報を見つけるのに大分彷徨ってしまったり、見つけたコードの記法が少々古かったり、ポイントとなる subject の指定方法がやや不適切だったりしたので備忘を兼ねてコードを残しておきます。

認証コード

一応 googleapis のバージョンは 67.0.0 です。古いバージョン(39.2.0とか)でも動きます。

const { google } = require('googleapis');
const credentials = require('./credentials.json'); // サービスアカウント秘密鍵jsonファイル
// ドメイン全体の委任で承認されている OAuth スコープの中から必要な分だけ指定する
const scopes = [
  'https://www.googleapis.com/auth/classroom.courses.readonly'
];
// API 呼出し主体となるアカウントを指定。無指定だとパーミッションエラーになる
const clientOptions = { subject: 'great-teacher@my-domain.com' };

async function main() {
  const auth = await google.auth.getClient({ credentials, scopes, clientOptions });
  const classroom = google.classroom({ version: 'v1', auth });

  // api 単位ではなくグローバルオプションとして認証情報をセットする場合は下記のようにする
  // const classroom = google.classroom({ version: 'v1' });
  // google.options({ auth });

  const response = await classroom.courses.list();
  console.log(JSON.stringify(response));
}

main().catch(err => {
  console.error(err);
  process.exit(1);
});

ポイント

getClient のパラメーターとして subject を指定した clientOptions を渡す点が重要です。
これを指定しないと 403 エラー (The caller does not have permission) が返ってきます。
逆に指定した場合はそのアカウントで認証して API を呼び出した場合と同じ結果が返ってきます。
例えば Classroom において教師A が課題1~10、教師B が課題11~20 を作成していた場合、
subject に教師A のアカウントを指定して courses.courseWork.list() を実行すれば課題1~10が、
教師B のアカウントを指定して実行すれば課題11~20 が返ってきます。

また、ここでは require を使って秘密鍵 Json ファイルをオブジェクトとして読み込んで credentials パラメーターに渡していますが、下記のようにファイルパスを keyFile パラメーターに渡しても良いです。

  const auth = await google.auth.getClient({ keyFile: './credentials.json', scopes, clientOptions });

付録:サービスアカウント作成とドメイン全体の委任

一応この記事だけ見れば良いようにサービスアカウント作成と、そのアカウントに対するドメイン全体の委任の手順を記載しておきます。ドメイン全体の委任についてはGoogle Workspaceの管理者しか実施できず、説明するためのスクショを撮るのもままならなかったりしますので。

OAuth 同意画面の設定

API とサービス > OAuth 同意画面
ここで設定したアプリ名が Google 管理コンソールでドメイン全体の委任を設定した際に表示されます。

OAuth 同意画面 の設定手順

OAuth 同意画面

下記のとおり設定し「作成」または「保存して次へ」

  • UserType:目的に応じて選択
  • アプリ情報
    • アプリ名:適切な名前を入力
    • ユーザーサポートメール:プルダウンから選択
    • アプリのロゴ:空でOK。ある場合は指定
  • アプリのドメイン:全て空でOK。ある場合は指定
  • 承認済みドメイン:追加なしでOK
  • デベロッパーの連絡先情報
    • メールアドレス:適切に指定

oauth-001.png
oauth-002.png

スコープ

なにも設定せず「保存して次へ」
oauth-003.png

テストユーザー

なにも設定せず「保存して次へ」
oauth-004.png

サービスアカウントの準備

API とサービス > 認証情報

サービスアカウントの準備手順

サービスアカウント作成

+ 認証情報の作成 から サービス アカウント を選択。
serviceaccount-001.png
下記のとおり設定して「完了」

  • サービス アカウント名:適当に(ここでは domain-delegation)
  • サービス アカウントID:自動設定
  • サービス アカウントの説明:空ないし任意の説明文

serviceaccount-002.png

ドメイン全体の委任を有効化

作成したサービスアカウントの編集ボタンをクリック。
serviceaccount-003.png

ドメイン全体の委任の表示 をクリックして展開。
G Suite ドメイン全体の委任を有効にする にチェックを入れて「保存」
serviceaccount-004.png

これにより「OAuth 2.0 クライアント ID」が表示されたことを確認し、再度サービスアカウントの編集ボタンをクリック。
serviceaccount-005.png

鍵を追加 プルダウンから 新しい鍵を作成 をクリック。
キーのタイプとして「JSON」を選択して「作成」し、秘密鍵Jsonファイルをダウンロード。
このファイルがソースコード上で require('./credentials.json') により取り込んでいた json ファイル。
serviceaccount-006.png
serviceaccount-007.png
serviceaccount-008.png

このサービスアカウント用の OAuth クライアント ID は一覧のコピーボタンから取得する。
serviceaccount-009.png

ドメイン全体の委任の承認

Google 管理コンソール > セキュリティ

ドメイン全体の委任の承認手順

セキュリティ項目一番下の API の制御 パネルをクリック。
workspace-001.png

API の制御項目の一番下の ドメイン全体の委任 にある「ドメイン全体の委任を管理」をクリック。
workspace-002.png

API クライアント一覧にある「新しく追加」をクリック。
workspace-003.png

クライアント ID にサービスアカウントの OAuth クライアント ID、スコープに利用する Google APIs に必要なスコープを必要な数だけ入力して「承認」。
(カンマ区切りと書いてあるが、1 つ入力するたびにテキストボックスが増えていくので 1 つずつ入力すればよい)
workspace-004.png
workspace-005.png

おわりに

探し方が悪いのかサービスアカウント認証関連の公式ドキュメントを探しても subject を指定しなきゃダメという情報を見つけられず、参考にさせて頂いた記事を見てようやく認証を通すことができました。
一応 subject の指定方法は適切な形になっているはずですし、async/await 版でのサンプルにもなっていると思いますので参考になれば。

参考

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

Amazon Transcribeをストリーミングで実行する

はじめに

  • Amazon Transcribe は音声データをテキスト化する(文字起こしとも言われる)サービスです
  • 音声ファイルを準備してバッチ処理するタイプと、音声をストリーミングして、リアルタイム(逐次的に)処理するタイプがあります
  • 日本語については、2020年11月にリアルタイム処理タイプに対応しました
  • AWSコンソール上ではブラウザの音声をTranscribeへストリーミングすることで機能を確認することができます。レスポンス良くテキスト化されます
  • 実際の業務等ではマネジメントコンソールで使うことはなく、アプリケーション等に組み込むことになると思います
  • ということで、サンプル的にストリーミング処理を実装してみました
    • そして自分への備忘も兼ねて処理内容の解釈を記述します
  • なお、SDK v3はpreviewなので、これからリリースされる正式バージョンと異なる(動作しない)可能性があります

環境

  • Ubuntu 20.04 on WSL2
  • Node.js v14.15.4
  • AWS SDK for JavaScript version 3(preview)

サンプルコード

下記コードをタタキ台として説明します

実行手順

オーディオファイルを準備する

  • バッチ処理ではなくリアルタイムストリーム処理、と言ったわりには音声ファイルを準備します
  • 音声入力部分も何らかのストリームにする想定ですが、今回はTranscribeとのストリーム部分にフォーカスし、シンプルにしました

    • (手元ではKinesisVideoStreamからの音声をTranscribeへパイプできることまで確認しています)
  • オーディオファイルの仕様

    • 符号付き16ビット PCM 8KHz RAW(ヘッダ無)
    • 指定がある場合はリトルエンディアンを選択
  • Audacity を利用して変換するのが簡単です

image.png

実行環境の準備

  • Node.jsの環境作成
    • 各自準備してください
  • githubからソースファイルを手元へコピー
    • git clone などで
  • パッケージインストール
    • npm install

実行

  • ニュース動画の音声で試してみました
  • 下記のようになります
  • 音声が30秒ぐらいで、処理時間も30秒+αかかります
  • アナウンサーの発声ということもあってか、正確にテキスト化されています
    • 来月 な の か? 部分が正確には 来月七日 ですが、他は誤変換もなく素晴らしい精度です
$ node transcription.js sample_audio.raw
0.08: きょう 東京 都心 は ぽかぽか 陽気 と なり 最高 気温 は 昨日 より 十 一 度、 以上 高い 十 八、 七 度 と か
8.39: 四 月 中旬 並み の 暖か さ と なり まし た
11.98: 江戸川 区 の 葛西 臨海 公園 に は 多く の 家族 連れ の 姿 が。
17.54: 東京 都 で は 緊急 事態 宣言 の 解除 が 予定 さ れ て いる 来月 な の か? まで
23.15: 上野 動物 園 や 葛西 臨海 水 族 園 など 都立 の 動物 園 や 水 族 館 を 臨時 休業 に し て い ます

コードの解説

対象ソースファイル

const parseTranscribeStream... 部分

  • Transcribeからのレスポンス解釈部です
  • Transcribeからのレスポンスオブジェクトを表示用に編集しています
  • IsPartial == true の場合は変換途中を示していますので表示から省いています
    • 条件を外して実行してみると挙動がわかると思います

const audioSource = createReadStream 部分

  • 音声ファイルを読み込みしています
  • highWaterMarkで一度に読み込むサイズを1KBに抑えています
    • 大きいサイズを送信するとTranscribe側がエラーを返すことがあります

const audioStream = async function* 部分

  • 音声ファイルのデータ片を所定のオブジェクト形式に変換しています

const command = new StartStreamTranscriptionCommand 部分

  • Transcribeとのリアルタイムストリーム開始コマンドを作成しています
    • 日本語、PCM形式、8KHzであることを伝えています
    • 入力は前段で定義したストリームを指定しています
  • 処理が開始すると、入力ストリームから順次データを受け取り、処理する記述です

const client = new TranscribeStreamingClient 部分

  • Transcribeとのリアルタイムストリーム開始リクエストクライアントを作成しています
    • パラメータ requestHandler でセッションタイムアウトを5秒指定しています
      • これを指定しないと音声データ送信が終了しても5分ぐらい通信が切断されません
      • SDK内部でコネクションプールしていますが、データ終了時にクローズせず、プールしたままになっているようです
      • SDKはまだpreviewなので改善するかもしれません

const response = await client.send(command) 部分

  • Transcribeとのリアルタイムストリーム開始リクエストしています
    • SDK v3では基本的に「コマンド作成」して、client.send()する記述になるようですね

const transcriptsStream = Readable.from(response.TranscriptResultStream) 部分

  • Transcribeからのレスポンスを受け取るストリームを宣言しています

transcriptsStream.pipe(parseTranscribeStream).pipe(stdout) 部分

  • ストリームをパイプで接続しています
    • Transcribeからのレスポンスを冒頭で記述したレスポンス解釈部へ
    • レスポンス解釈部のアウトプットを標準出力へ

まとめ

  • Node.jsの非同期ストリーム処理によってシンプルに実装できる印象です
    • そのぶん、どこで何が処理されているか理解しておかないとハマるかもしれません
  • この実装だと、HTTP/2 Streaming 動作になると思います
    • 実は SDK v2で WebSockets Streaming も試していたのですが、途中でv3に気づいて移ってきました
    • あまり詳しくないのですが、HTTP/2ストリーミングだと送信ごとに毎回署名してるようなので効率は落ちるのでしょうかね :thinking:
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js × Expressで始めるGraphQL入門

概要

GraphQLを初めて実装してみた時の手順と、サンプルソースコードを記載しています。

環境

  • macOS Big Sur 11.1
  • node v14.12.0
  • yarn 1.22.10
  • express 4.16.1
  • Docker version 19.03.13
  • docker-compose version 1.27.4

サンプルソースコード

hayatoiwashita/express-graphql

手順

1. プロジェクトの雛形を作成

express-generatorを使用した。
--gitオプションは.gitignoreを追加するためのオプション。

$ express --view=pug --git express-graphql

参考: Express のアプリケーション生成プログラム

2. ライブラリのインストール

GraphQL関連のライブラリは最低限下記があればOK。

$ yarn add graphql
$ yarn add express-graphql

サンプルでは下記も追加している(本題から逸れるので割愛)

# ホットリロード
$ yarn add --dev nodemon
# コード整形
$ yarn add prettier --dev --exact
$ yarn add eslint --dev
$ yarn add --dev eslint-config-prettier
$ yarn add --dev eslint-plugin-prettier
# commit時にコード整形を実行
$ yarn add --dev lint-staged
$ yarn add --dev husky

3. サンプルデータの用意

/data/users.jsを追加して、DBの代わりに使用した。
(内容はサンプルソースコードのusers.jsを参照)

4. schemaを定義

/schema/schema.graphqlを作成してschemaを定義。
今回は参照系クエリのみ実装した。
データ構造は、ユーザとそれに紐づく投稿といった形。

schema.graphql
const { buildSchema } = require("graphql");

const schema = buildSchema(`
    type Query {
        users: [User!]!,
        user(id: Int!): User!
    }

    type User {
        id: ID!
        name: String!
        email: String
        posts: [Post!]
    }

    type Post {
        id: ID!
        title: String!
        published: Boolean!
        link: String
        author: User!
    }
`);

module.exports = schema;

5. リゾルバを作成

データ操作を行う/src/resolvers.jsを作成。
上述の通り、今回は参照系のみ。

resolvers.js
const { Users } = require('../data/users');

const resolvers = {
  users: async (_) => {
    console.log('come here');
    return Users;
  },
  user: async ({ id }, context) => {
    return Users.find((user) => user.id == id);
  },
};

module.exports = resolvers;

6. app.js を修正

エンドポイント/graphqlを定義。

app.js
// var createError = require('http-errors');
var express = require('express');
var { graphqlHTTP } = require('express-graphql');
var resolvers = require('./src/resolvers');
var schema = require('./schema/schema.graphql');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use(
  '/graphql',
  graphqlHTTP({
    schema,
    rootValue: resolvers,
    graphiql: true,
  })
);

module.exports = app;

7. [参考] コンテナ化

Dockerfiledocker-compose.ymlを追加。

FROM node:14.15.4-slim

WORKDIR /usr/local/src/express-graphql
ADD . .
RUN yarn install
CMD [ "yarn", "start" ]
docker-compose.yml
version: '3'

services:
  graphql:
    build: .
    container_name: graphql-container
    ports:
      - "3000:3000"

8. Expressの起動

起動コマンドを実行。

# コンテナで実行
$ docker-compose up

# コンテナを使用せず実行
$ yarn start

9. クエリを実行

http://localhost:3000/graphql をブラウザで開く。
screencapture-localhost-3000-graphql-2021-01-16-16_08_50.png

サンプルクエリ
内容としては、ユーザID=1のユーザの名前と、そのユーザの投稿(posts)のIDとタイトルを取得するというもの。

{
  user(id:1) {
    name
    posts {
      id
      title
    }
  }
}

実行結果

{
  "data": {
    "user": {
      "name": "Fikayo Adepoju",
      "posts": [
        {
          "id": "1",
          "title": "Debugging an Ionic Android App Using Chrome Dev Tools"
        },
        {
          "id": "2",
          "title": "Hosting a Laravel Application on Azure Web App"
        }
      ]
    }
  }
}

screencapture-localhost-3000-graphql-2021-01-17-15_09_38.png

参考

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

Azure IoT Hub に Node.js で publish

Ubuntu 20.10 で確認しました。

ライブラリーのインストール

sudo npm install -g azure-iot-device-mqtt
sudo npm install -g azure-iot-device
azure_publish.js
#! /usr/bin/node
// ---------------------------------------------------------------
//  azure_publish.js
//
//                  Jan/20/2021
//
// ---------------------------------------------------------------
'use strict'
var devicemqtt = require('azure-iot-device-mqtt')
var device = require('azure-iot-device')
const dotenv = require('dotenv')
//
// ---------------------------------------------------------------
function printResultFor(op) {
    return function printResult(err, res) {
    if (err) console.log(op + ' error: ' + err.toString())
    if (res) console.log(op + ' status: ' + res.constructor.name)
    }
}
// ---------------------------------------------------------------
function define_data_proc ()
{
    const device_id = "atami"
    const jikan= new Date()
    const hour = jikan.getHours()
    const minute = jikan.getMinutes()
    const second = jikan.getSeconds()
    const current_time = "" + hour + ":" + minute + ":" + second
        const tt = (20 + (Math.random() * 15)) * 10
        const temperature = Math.round (tt) / 10
        const hh = (60 + (Math.random() * 20)) * 10
        const humidity = Math.round (hh) / 10
        const ddx = { device_id: device_id, time: current_time, temperature: temperature, humidity: humidity }
        const data = JSON.stringify(ddx)

    return data
}

// ---------------------------------------------------------------
console.error ("*** 開始 ***")
dotenv.config()
const connectionString = `${process.env.CONNECTSTRING}`
console.log(connectionString)

var Message = device.Message

var client = devicemqtt.clientFromConnectionString(connectionString)

var connectCallback = function (err) {
if (err)
    {
    console.error('Could not connect: ' + err);
    }
else
    {
    console.log('Client connected')

    const data =  define_data_proc ()
    var message = new Message(data)
    console.log("Sending message: " + message.getData())
    client.sendEvent(message, printResultFor('send'))
console.error ("*** ppp ***")
    }
console.error ("*** qqq ***")
}

client.open(connectCallback)
console.error ("*** rrr ***")

console.error ("*** 終了 ***")
// ---------------------------------------------------------------
.env
CONNECTSTRING="HostName=iot-aa.azure-devices.net;DeviceId=pansy;SharedAccessKey=SvsFWVsCy2bJkH0QuPJeIabcdefgh8mo6S6vNCjom92="

プライマリ接続文字列 をポータルで調べる必要があります。
primary_jan17.png

次のコマンドでも調べることが出来ます。

az iot hub device-identity connection-string show --hub-name iot-aa \
    --device-id pansy --output table

実行コマンド

export NODE_PATH=/usr/lib/node_modules
./azure_publish.js

関連情報
Azure IoT Hub に Node.js で subscribe

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

【Promiseとは】

【Promiseとは】

Promiseはrejectedかfulfilledという結果が帰ってくることを約束してくれる。

そのため、それらに対するハンドリングがしやすいといったメリットがあると勉強していて感じた。実際、Node.jsの各メソッドをPromiseインスタンスに変換できるようなものが導入されたりとそのうちPromiseの規格に統一されるかもしれない。

【Promiseの挙動】

Promiseは、Promiseインスタンスを返す。

その時、Promiseインスタンスは最終的に、rejectedかfulfilledを返すが、それは以下のようにpendingな状態をreject()かresolve()で解決しないといけない。

         pending
       ↓
      / \
    reject()   resolve()
       ↓         ↓
  rejected     fulfilled    <=  これらの状態をsettledという

【Promiseインスタンスをsettled状態にしてみる】

まずPromiseはnew Promise() のようにして一度pendingな状態にする方法と、

pending状態を経ずに直接fulfilled、rejected状態にする2パターンある。

// 一度pendingにする
// settledにするには一度変数に入れて、setTimeoutで3000経過させるなどしないといけない
function asyncFunc(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            try{ 
                resolve('成功')
            }catch(err){
                reject(err)
        }},3000)
    })
}
console.log(asyncFunc()) => Promise { <pending> }

// 直接resolve、rejected
Promise.resolve('成功') => Promise { '成功' }
Promise.reject(new Error('エラー')) => Promise { <rejected> Error: エラー ~~(省略)

【settled状態を利用して次の処理をおこなう】

.then()、.catch()、を使えば、Promiseインスタンスがsettledになったら、実行するコールバック処理を設置できる。

以下の処理は、Promiseが1でfulfilled状態になり、その結果を.then()のnumで受け取っている。

Promiseでコールバックヘルが解決できるのはこういったことができるからだ。

Promise.resolve(1)
             .then(num => console.log('resolve', num))

上記の.then()は省略されており、第二引数には.then()で起きたエラーの処理を受け取れるようになっている。しかし、これは.then()が多くなると長くなる。

.catch()を最後につけることで.then()で起きたエラーを全て一箇所で受け取れる。

// 同じことをしている
Promise.resolve(1)
             .then(
                    num => console.log('resolve', num),
                    err => new Error('エラー')
             )

Promise.resolve(1)
             .then(num => console.log('resolve', num))
             .catch(err => new Error('エラー'))

【複数のPromiseを並行に処理してみる】

・ Promise.all 

中のPromiseインスタンスが全部fulfilledになったら、その値でfulfilledを返す。

これだと各処理が平行に実行されるので効率がいい。

const all = Promise.all(
    [1,Promise.resolve('成功'), Promise.resolve('fulfilled')]
)
all => Promise { [ 1, '成功', 'fulfilled' ] }

・Promise.allSettled

中のPromiseインスタンスがfulfilledかrejectedかに関わらずそれぞれの結果を返す。

とりあえず結果をきにせず平行に処理したい時に便利かも。

const allSettled = Promise.allSettled([
    Promise.resolve('成功'), Promise.reject(new Error('エラー'))
])
allSettled => Promise {[
    { status: 'fulfilled', value: '成功' },
    { status: 'rejected',
      reason: Error: エラー ~ (省略)

Node.jsでよく使うasync awaitもPromiseインスタンスをawaitの直前に置くことで実装できるように設計されているためPromiseの理解が重要。

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

【Promiseは規格?みたいなもの】

【Promiseとは】

Promiseはrejectedかfulfilledという結果が帰ってくることを約束してくれる。

そのため、それらに対するハンドリングがしやすいといったメリットがあると勉強していて感じた。実際、Node.jsの各メソッドをPromiseインスタンスに変換できるようなものが導入されたりとそのうちPromiseの規格に統一されるかもしれない。

【Promiseの挙動】

Promiseは、Promiseインスタンスを返す。

その時、Promiseインスタンスは最終的に、rejectedかfulfilledを返すが、それは以下のようにpendingな状態をreject()かresolve()で解決しないといけない。

         pending
       ↓
      / \
    reject()   resolve()
       ↓         ↓
  rejected     fulfilled    <=  これらの状態をsettledという

【Promiseインスタンスをsettled状態にしてみる】

まずPromiseはnew Promise() のようにして一度pendingな状態にする方法と、

pending状態を経ずに直接fulfilled、rejected状態にする2パターンある。

// 一度pendingにする
// settledにするには一度変数に入れて、setTimeoutで3000経過させるなどしないといけない
function asyncFunc(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            try{ 
                resolve('成功')
            }catch(err){
                reject(err)
        }},3000)
    })
}
console.log(asyncFunc()) => Promise { <pending> }

// 直接resolve、rejected
Promise.resolve('成功') => Promise { '成功' }
Promise.reject(new Error('エラー')) => Promise { <rejected> Error: エラー ~~(省略)

【settled状態を利用して次の処理をおこなう】

.then()、.catch()、を使えば、Promiseインスタンスがsettledになったら、実行するコールバック処理を設置できる。

以下の処理は、Promiseが1でfulfilled状態になり、その結果を.then()のnumで受け取っている。

Promiseでコールバックヘルが解決できるのはこういったことができるからだ。

Promise.resolve(1)
             .then(num => console.log('resolve', num))

上記の.then()は省略されており、第二引数には.then()で起きたエラーの処理を受け取れるようになっている。しかし、これは.then()が多くなると長くなる。

.catch()を最後につけることで.then()で起きたエラーを全て一箇所で受け取れる。

// 同じことをしている
Promise.resolve(1)
             .then(
                    num => console.log('resolve', num),
                    err => new Error('エラー')
             )

Promise.resolve(1)
             .then(num => console.log('resolve', num))
             .catch(err => new Error('エラー'))

【複数のPromiseを並行に処理してみる】

・ Promise.all 

中のPromiseインスタンスが全部fulfilledになったら、その値でfulfilledを返す。

これだと各処理が平行に実行されるので効率がいい。

const all = Promise.all(
    [1,Promise.resolve('成功'), Promise.resolve('fulfilled')]
)
all => Promise { [ 1, '成功', 'fulfilled' ] }

・Promise.allSettled

中のPromiseインスタンスがfulfilledかrejectedかに関わらずそれぞれの結果を返す。

とりあえず結果をきにせず平行に処理したい時に便利かも。

const allSettled = Promise.allSettled([
    Promise.resolve('成功'), Promise.reject(new Error('エラー'))
])
allSettled => Promise {[
    { status: 'fulfilled', value: '成功' },
    { status: 'rejected',
      reason: Error: エラー ~ (省略)

Node.jsでよく使うasync awaitもPromiseインスタンスをawaitの直前に置くことで実装できるように設計されているためPromiseの理解が重要。

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

Node.js で車とシリアル通信する

先日車が壊れたのを機に車について調べていたら OBD (On-board diagnostics) という診断機能を知りました。大抵の車には OBD のコネクタが搭載されていて、これに ELM327というマイコンを使った製品をつなぐと Bluetooth のシリアル通信などで簡単に情報をやりとりできるそうです。おもしろそうなので試してみました。

重要 このポストと同じ方法で車の診断ログをリセットしたりすることもできます。診断ログが正しくない状態になると、修理の適切なタイミングなどがわからなくなるはずです。コマンドを車に送る前によく考えた上で自己責任で試してください。


ELM327 のドングルを手に入れる

これらの製品は Amazon 等で廉価に入手できるようです。全製品で固定っぽい Bluetooth の接続パスワードが書かれていたので念のため黒塗りにしました。

connector1.jpg

車のOBDポートを探す

運転席のどこかにある場合が多いようです。私の車はここにありました。

connector2.jpg

ここに購入した上記のドングル型デバイスを接続します。

通信方法を確認

送信

AT コマンドというものを送るとマイコンを操作することができるようです。今回は以下の2つのコマンドを使いました。

  • AT Z: 状態をリセットする
  • AT SP 0: OBDの通信プロトコルを自動で検出

通信プロトコルは様々なものがあるようですが、私の車で試した限りでは AT SP 0 の自動検出でうまく通信できました。

AT コマンドの他に OBD コマンドというものがあり、これで様々な OBD の情報を取得できました。Wikipedia にコマンドのリストがあったため、これを参照しました。

OBD コマンドは Mode と PID から構成されています。現在のデータを読み取る 01 というモードを今回は使いました。取得できるデータごとに PID が振られており、例えば PID0C のエンジン回転数の情報を読み取る OBD コマンドは 010C になります。

受信

データは、41 0C AA BB ... のようにスペース区切りの16進数の文字列で帰ってきます。1つめが Mode の値に40を足したもの、2つめが PID で、それ以降が実際のデータになります。例えばエンジン回転数の場合は値が4倍されているなど、それぞれ決まりがあるようです。

Node.js でシリアル通信をする

serialport というライブラリが著名そうだったので利用しました。

Node.js のランタイムでシリアルポートをオープンする TypeScript のコードはこんな感じ。ELM327 のデバイスに Bluetooth で接続した後に connect の関数を実行すると、非同期で SerialPort のインスタンスが得られます。

import SerialPort from 'serialport'

const DEVICE_PATH = '/dev/tty.OBDII-SPPslave'
export const connect = () => new Promise<SerialPort>((resolve, reject) => {
    const port = new SerialPort(DEVICE_PATH, { baudRate: 9600, autoOpen: false })
    console.log(`[SP] opening on ${DEVICE_PATH}..`)
    port.open((error) => {
        if(error) {
            console.log('[SP] port opening error.')
            reject(error)
        } else {
            console.log('[SP] port opened.')
            resolve(port)
        }
    })
})

ポートに書き込むには write メソッドを使います。コマンドのデリミタはキャリッジリターンなので注意してください。

// `AT Z` の AT コマンドを送出
port.write('AT Z\r')

データはイベントリスナーを使って取得できます。
文字列を処理して、目的の値を抽出します。

port.on('data', (data: string) => {
    const [mode, pid, ...values] = data.split(' ')

    let modeNum
    try {
        modeNum = parseInt(mode, 16) - 40  
    } catch {
        return
    }

    if(modeNum === 1) {
        const hexValue = parseInt(values.filter(value => value !== '').join(''), 16)
        if(pid === '0C') {
            console.log('EngineRPM: ' + hexValue / 4)
        }
    }
})

WebSocket でデータをブラウザに送信できるようにする

上記でデータが取得できる準備が整ったので、あとは WebSocket を使ってブラウザにデータを送ってみます。ws というライブラリを使います。

WebSocket サーバーを立ち上げるコードはこんな感じ。 createSocket の関数をコールすると非同期で WebSocket のインスタンスが得られます。

import WebSocket from 'ws'

export const createSocket = () => new Promise<WebSocket>((resolve, reject) => {
    console.log('[WS] connecting...')
    const wss = new WebSocket.Server({ port: 8081 })
    wss.on('connection', (socket) => {
        console.log('[WS] connected.')
        resolve(socket)
    })
})

WebSocket のインスタンスを使って、シリアル通信で得られたデータを送ります。上記のシリアル通信のイベントリスナーに socket.send(data) を差し込めば OK です。

クライアント側は、こんな感じ。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #display {
            width: 500px;
            height: 100px;
            font-size: 2em;
        }
    </style>
</head>
<body>
    <textarea id="display"></textarea>
    <script>
        const ws = new WebSocket('ws://localhost:8081')
        const display = document.getElementById('display')

        let currentData = {}

        ws.onmessage = (event) => {
           const data = JSON.parse(event.data)
            display.value = "エンジン回転数: " + data.engineRPM + "rpm"
        }
    </script>
</body>
</html>

動作させる

以上で全ての材料が揃ったので、これを組み込んでいきます。今回は Mac 上で以下の全てのプロセスを立ち上げて試してみました。

  • 車とシリアル通信をする WebSocket サーバー
  • クライアントを表示する ウェブサーバー

また、エンジン回転数以外にも現在の運転速度と冷却水温度も取得してみました。

image.png
https://youtu.be/jtPIO3J6cEs

動画を撮りながら走ってみると、いい感じにデータが取れているようです✌️


上記のコードは以下のリポジトリにあります。
https://github.com/kamataryo/parse-obd-serial

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

Node.js(express)でpostgresSQLのSSL認証が通らないことについて

はじめに

店舗予約の受付をするLineBotの開発をするために
Node.js(express)を開発言語にしました。

PostgresSQLでDB作成をしたさいに
SSL接続が原因でエラーとバトルした件について解説します。

少しでもお役に立てれば幸いです。

誤った情報だと感じましたら教えてください!

開発環境

(1)macOS 10.15.7
(2)Node.js 12.19.0
(3)express 4.17.1
(4)line/bot-sdk" 7.2.0
(5)Postgres 8.5.1
(6)Heroku

エラー内容

index.js
const { Client } = require('pg');

//Postgres環境変数を設定//

const connection = new Client({
  user: process.env.PG_USER,
  host: process.env.PG_HOST,
  database: process.env.PG_DATABASE,
  password: process.env.PG_PASSWORD,
  port: ****
});
connection.connect();

//クエリ文//

const userTable = {
    text:'CREATE TABLE IF NOT EXISTS users (id SERIAL NOT NULL, line_uid VARCHAR(255), display_name VARCHAR(255), timestamp VARCHAR(255), trigger SMALLINT, remedy SMALLINT, pelvis SMALLINT);'
 };

//クエリを実行するためのコード//

connection.query(userTable)
   .then(()=>{
       console.log('table users created successfully!!');
   })
   .catch(e => console.log(e));

テーブルができているか確認をするために

ターミナル
$ heroku pg:psql

データベースモードになったら

ターミナル
DATABASE=> select * from users; 
ここで怒られます。 orz
ターミナル
ERROR:  relation "users" does not exist  //"users"との関係性がありません

LINE 1: select * from "users";

((((;゚Д゚)))))))

解決策

エラー文を検索しても出てくる記事が

1、select * from "users"とダブルクォーテーションで囲め!!

2、大文字と小文字が〜〜〜

など変更しても解決にならず。

どうしても分からず質問したところ
Postgres環境変数を修正しましょう!!これで解決できます!!と素敵なアドバイスをもらいました♫

index.js
//Postgres環境変数を設定//

const connection = new Client({
  connectionString: process.env.DATABASE_URL,
  ssl: {
    rejectUnauthorized: false    //SSL接続なくても許可するよーと宣言
  }
});
connection.connect();

コードの変更をしたので確認します!!!

先程と同じようにデータベースモードにします。

ターミナル
$ heroku pg:psql
ターミナル
DATABASE=> select * from users;

上記にコードを修正した結果

ターミナル
ファイル名::DATABASE=> select * from users;
 id | line_uid | display_name | timestamp | trigger | remedy | pelvis 
----+----------+--------------+-----------+---------+--------+--------
(0 rows)

おけ?

考察

今回のエラーはSSL接続が許可されておらず認証されないことによって起きたと考えています。

(1)最近仕様が変更になった出来事なのか?

(2)SSL接続がデフォルトに変更されたのか?

参考文献が見つからず原因がわかる方がいらっしゃいましたら、ご教授ください。

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

M5Core2のLCDにWebページのスクリーンショットを表示する

M5Core2のLCDにいろんな情報を表示する際に、画面レイアウトを試行錯誤しながらコンパイル・書き込み・実行を繰り返すのは手間なので、HTMLで画面を作成してそのスクリーンショットをM5Core2のLCDに表示するようにします。

image.png

もろもろのソースコードをGitHubに上げておきました。

poruruba/WebSnapshot
 https://github.com/poruruba/WebSnapshot

スクリーンショット生成

スクリーンショットには「puppeteer」を使います。

puppeteer/puppeteer
 https://github.com/puppeteer/puppeteer

(参考)呼び出し方
 https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md

サーバの実装は以下の通りです。

api/controllers/screenshot/index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const BinResponse = require(HELPER_BASE + 'binresponse');

const { URL, URLSearchParams } = require('url');
const fetch = require('node-fetch');
const Headers = fetch.Headers;

const puppeteer = require('puppeteer');

exports.handler = async (event, context, callback) => {
    if( event.path == '/screenshot' ){
        var url = event.queryStringParameters.url;
        var wait = 0;
        var width = 640;
        var height = 480;
        var scale = 1.0;
        var type = event.queryStringParameters.type || 'png'; // png or jpeg
        if( event.queryStringParameters.width )
            width = parseInt(event.queryStringParameters.width);
        if( event.queryStringParameters.height )
            height = parseInt(event.queryStringParameters.height);
        if( event.queryStringParameters.scale )
            scale = parseFloat(event.queryStringParameters.scale);
        if( event.queryStringParameters.wait )
            wait = parseInt(event.queryStringParameters.wait);
        console.log(width, height, scale, url);

        var browser = await puppeteer.launch();
        var page = await browser.newPage();
        await page.setViewport({
            width: width,
            height: height,
            deviceScaleFactor: scale,
        });
        await page.goto(url, { waitUntil: "load" });
        if( event.queryStringParameters.waitfor ){
            try{
                await page.waitForFunction("vue.render.loaded");
            }catch(error){
                console.log(error);
            }
        }
        if( wait > 0 )
            await page.waitForTimeout(wait);
        var buffer = await page.screenshot({ type: type });
        browser.close();

        return new BinResponse('image/' + type, buffer);
    }else
    if( event.path == '/screenshot-weather'){
        var location = parseInt(event.queryStringParameters.location);
        var weather = await do_get_weather(location);

        return new Response({ weather });
    }
};

/* location: 13:東京、14:神奈川 */
function do_get_weather(location){
    return fetch('https://www.drk7.jp/weather/json/' + location + '.js', {
            method : 'GET'
    })
    .then((response) => {
            return response.text();
    })
    .then(text =>{
            text = text.trim();
            if( text.startsWith('drk7jpweather.callback(') )
                    text = text.slice(23, -2);
            return JSON.parse(text);
    });
}

少し解説します。
puppeteerは、内部的にはChrome(またはFirefox)を使っています。
以下の部分で、ブラウザの起動と、ページタブの生成をしています。

api/controllers/screenshot/index.js
var browser = await puppeteer.launch();
var page = await browser.newPage();

この部分で、ブラウザの表示サイズを変更しています。

api/controllers/screenshot/index.js
        await page.setViewport({
            width: width,
            height: height,
            deviceScaleFactor: scale,
        });

M5Core2は、320×240であるため、width=320、height=240、scale=1.0で良いかと思います。場合によっては、解像度が小さすぎてHTMLレイアウトが難しい場合は、例えば、width=640、height=480、scale=0.5 のようにして、いったん大きい画面でレンダリングしたのち、縮小表示して320×240に合わせるというやり方も可能です。

以下の部分でURLで示されるWebページを取得しレンダリングします。

api/controllers/screenshot/index.js
        await page.goto(url, { waitUntil: "load" });

        if( event.queryStringParameters.waitfor ){
            try{
                await page.waitForFunction("vue.render.loaded");
            }catch(error){
                console.log(error);
            }
        }
        if( wait > 0 )
            await page.waitForTimeout(wait);

その中で、必要に応じてwaitFor*** を呼び出しています。
Javascriptで画面を制御している場合、Javascriptの処理が終わった後に画面キャプチャするためのものです。
waitForTimeoutはウェイト時間を決めてその時間経過後に画面キャプチャするもので、waitForFunctionはWebページ中のJavascriptの条件式で待ち終了させます。
waitForFunctionの方は、WebページのJavascriptの実装に依存するので、お好みで変えてください。

以下の部分が、画面キャプチャする部分です。

api/controllers/screenshot/index.js
var buffer = await page.screenshot({ type: type });

JpegかPNGが選べます。

最後に、ブラウザを閉じて終わりです

api/controllers/screenshot/index.js
browser.close();

この画像バイナリを呼び出し元に返します。

api/helpers/binresponse.js
class BinResponse{
    constructor(content_type, context){
        this.statusCode = 200;
        this.headers = {'Access-Control-Allow-Origin' : '*', 'Cache-Control' : 'no-cache', 'Content-Type': content_type };
        this.isBase64Encoded = true;
        if( context )
            this.set_body(context);
        else
            this.body = "";
    }

    set_filename(fname){
        this.headers['Content-Disposition'] = 'attachment; filename="' + fname + '"';
        return this;
    }

    set_error(error){
        this.body = JSON.stringify({"err": error});
        return this;
    }

    set_body(content){
        this.body = content.toString('base64');       
        return this;
    }

    get_body(){
        return Buffer.from(this.body, 'base64');
    }
}

module.exports = BinResponse;

以下の部分は、スクリーンショット生成には関係しませんが、のちほどスクリーンショット対象のWebページで使っているものです。

api/controllers/screenshot/index.js
        var weather = await do_get_weather(location);

M5Core2の実装

M5Core2側の実装です。

WebSnapshot/src/main.cpp
#include <WiFi.h>
#include "M5Lite.h"
#include <HTTPClient.h>

const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";

const char* screenshot_url = "【Node.jsサーバのURL】/screenshot";
const char* target_url = "【スクリーンショット対象のWebページのURL】";

#define SCREENSHOT_INTERVAL   (10 * 60 * 1000) //スクリーンショット取得の間隔
#define DISPLAY_WIDTH   320  //LCDの横解像度
#define DISPLAY_HEIGHT  240 //LCDの縦解像度
#define SCREENSHOT_SCALE  1.0 //スクリーンショットの表示倍率
#define BUFFER_SIZE   20000 //画像受信のバッファサイズ

unsigned char buffer[BUFFER_SIZE];

void wifi_connect(const char *ssid, const char *password);
String urlencode(String str);
long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len, unsigned short timeout);

void setup() {
  M5Lite.begin();
  Serial.begin(9600);
  Serial.println("setup");

  wifi_connect(wifi_ssid, wifi_password);
  Serial.println("connected");
}

void loop() {
  M5Lite.update();

  String url = screenshot_url;
  url += "?type=jpeg&width=" + String((int)(DISPLAY_WIDTH / SCREENSHOT_SCALE)) + "&height=" + String((int)(DISPLAY_HEIGHT / SCREENSHOT_SCALE)) + "&scale=" + String(SCREENSHOT_SCALE);
//  url += "&waitfor=true";
  url += "&wait=5000"; 
  url += "&url=" + urlencode(target_url);
  Serial.println(url);
  unsigned long length = sizeof(buffer);
  long ret = doHttpGet(url, buffer, &length, 5000);
  if( ret == 0 ){
    M5Lite.Lcd.drawJpg(buffer, length);
  }

  delay(SCREENSHOT_INTERVAL);
}

void wifi_connect(const char *ssid, const char *password){
  Serial.println("");
  Serial.print("WiFi Connenting");
  M5Lite.Lcd.println("WiFi Connectiong");

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    M5Lite.Lcd.print(".");
    delay(1000);
  }
  Serial.println("");
  Serial.print("Connected : ");
  Serial.println(WiFi.localIP());
  M5Lite.Lcd.println("");
  M5Lite.Lcd.print("Connected : ");
  M5Lite.Lcd.println(WiFi.localIP());
}

String urlencode(String str){
    String encodedString = "";
    char c;
    char code0;
    char code1;
//    char code2;
    for (int i = 0 ; i < str.length() ; i++){
      c = str.charAt(i);
      if (c == ' '){
        encodedString += '+';
      } else if (isalnum(c)){
        encodedString += c;
      } else{
        code1 = (c & 0xf) + '0';
        if ((c & 0xf) > 9){
            code1 = (c & 0xf) - 10 + 'A';
        }
        c = (c >> 4) & 0xf;
        code0 = c + '0';
        if (c > 9){
            code0 = c - 10 + 'A';
        }
//        code2 = '\0';
        encodedString += '%';
        encodedString += code0;
        encodedString += code1;
        //encodedString+=code2;
      }
      yield();
    }
    return encodedString;
}

long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len, unsigned short timeout){
  HTTPClient http;

  http.setTimeout(timeout + 5000);

  Serial.print("[HTTP] GET begin...\n");
  // configure traged server and url
  http.begin(url);

  Serial.print("[HTTP] GET...\n");
  // start connection and send HTTP header
  int httpCode = http.GET();
  unsigned long index = 0;

  // httpCode will be negative on error
  if(httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      Serial.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if(httpCode == HTTP_CODE_OK) {
        // get tcp stream
        WiFiClient * stream = http.getStreamPtr();

        // get lenght of document (is -1 when Server sends no Content-Length header)
        int len = http.getSize();
        Serial.printf("[HTTP] Content-Length=%d\n", len);
        if( len != -1 && len > *p_len ){
          Serial.printf("[HTTP] buffer size over\n");
          http.end();
          return -1;
        }

        // read all data from server
        while(http.connected() && (len > 0 || len == -1)) {
            // get available data size
            size_t size = stream->available();

            if(size > 0) {
                // read up to 128 byte
                if( (index + size ) > *p_len){
                  Serial.printf("[HTTP] buffer size over\n");
                  http.end();
                  return -1;
                }
                int c = stream->readBytes(&p_buffer[index], size);

                index += c;
                if(len > 0) {
                    len -= c;
                }
            }
            delay(1);
        }
      }
  } else {
    http.end();
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    return -1;
  }

  http.end();
  *p_len = index;

  return 0;
}

大した処理はしていないです。
なぜならば、ESP32 Lite Pack Library に含まれているLovyanGFXがすべてをやってくれているからです。
本来であれば、M5Core2の回路を見てLCDに表示できるようにしたり、Jpegを解析したりと、いろいろやらないといけないのですが、機種判別やらJpegのLCD表示やらをすべてやってくれているからです。

以下は、環境に合わせて以下を変更してください。

WebSnapshot/src/main.cpp
const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";

const char* screenshot_url = "【Node.jsサーバのURL】/screenshot";
const char* target_url = "【スクリーンショット対象のWebページのURL】";

#define SCREENSHOT_INTERVAL   (10 * 60 * 1000) //スクリーンショット取得の間隔
#define DISPLAY_WIDTH   320  //LCDの横解像度
#define DISPLAY_HEIGHT  240 //LCDの縦解像度
#define SCREENSHOT_SCALE  1.0 //スクリーンショットの表示倍率
#define BUFFER_SIZE   20000 //画像受信のバッファサイズ

platformio.iniは以下にしました。

WebSnapshot/platformio.ini
[env:m5stack-core2]
platform = espressif32
board = m5stack-fire
framework = arduino
upload_port = COM8
monitor_port = COM8
lib_deps =
  https://github.com/m5stack/M5Core2.git
  tanakamasayuki/ESP32 Lite Pack Library@^1.3.2

M5Core2に表示するWebページの作成

HTMLはもちろん、CSSやJavascriptも使えますので、通常のWebページ作成時の要領と同じです。
ただし、小さい解像度である320x240では表示が崩れてしまう可能性大なので、Chromeブラウザ上で試行錯誤したいところ。
Chromeの開発ツールを使えばよいです。

image.png

F12キーを押して開発ツールを表示させ、Elementsタブの左にある□2つのアイコンをクリックします。
次に、以下の部分でResponsiveが選択された状態にすると、表示領域の縦横の解像度を変更できるようになるので、ここで320x240とします。

image.png

これで、Webページのレイアウト作成がやりやすくなるかと思います。
完成したら、同じURLをM5Core2からNode.jsサーバに渡すとChromeの開発ツールで見えている状態のままのJpeg画像が取得され表示されます。

以上

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

MySQLで取得したデータを、EJSでWebブラウザに出力する

はじめに

こちらは、エンジニアの新たな学びキャンペーンに向けた記事となります。

Node.js + Express で作る Webアプリケーション 実践講座を参考にしながら、
データベース(以下DB)内のデータを、Webブラウザに表示する方法を記事にしました。

なお、ここではNode.jsのテンプレートエンジンであるEJSと、RDB(リレーショナルデータベース)であるMariaDB(MySQL)を利用します。

(上記講座ではMongoDBが利用されていますが、本記事ではMySQLに置き換えました。)

実行環境

  • Node.js v12.16.3
  • Express 4.16.1
  • 10.4.11-MariaDB

対象者

  • JavaScriptの文法自体は学んだけど、Web技術はまだほとんど学べていない人
  • かんたんなCRUD操作に関するSQLを理解している人

本記事でわかること

  • サーバーサイド言語Node.js & フレームワークExpressを使い、Hello Worldする方法
  • ExpressとMariaDB(MySQL)の連携方法
  • ExpressとMariaDB(MySQL)を使い、DB内のデータをWebブラウザ上に出力する方法

対象のUdemy講座で学んだこと

対象の講座で学んだことのうち、特に本記事へ反映する内容は以下となります。

  • パッケージマネージャであるnpmを使い、ミドルウェアやフレームワークを導入する方法
  • EJSの構文の使い方
  • Expressでのルーティング方法(特に関数の引数に関して)

EJSとは

EJSは、テンプレートエンジンと呼ばれるもののひとつで、
テンプレートエンジンはHTMLの中にプログラム言語を埋め込むことができます。

特にEJSにおいては、HTML文書の中に<% %>, <%= %>タグなどを埋め込み、この中にプログラムを記述します。

EJSの基本構文

EJSの基本的な書き方にきれいにまとまっていたので、
こちらを参照すると幸せになれます。

EJSの利点

EJSは、サーバーサイドで保持している変数の値を併用してHTMLを記述するとき、書きやすさ・読みやすさの点で非常に強力です。

例えばサーバーサイド言語のみでHTML文書を書く場合、次のようなソースコードになります。

app.js
const express = require("express");
const app = express();

app.get("/", (req, res) => {
  const text = "Hello World";
  let data = "<!DOCTYPE html>\r\n";
  data += "<html>\r\n";
  data += "  <head>\r\n";
  data += "    <meta charset='UTF-8'>\r\n";
  data += "    <title>hoge</title>\r\n";
  data += "  </head>\r\n";
  data += "  <body>\r\n";
  data += "<p>";
  data += text
  data += "</p>\r\n";
  data += "  </body>\r\n";
  data += "</html>";
  res.send(data);
});
app.listen(3000);

一方で、テンプレートエンジンを併用した場合は、次のようになります。
クォーテーションや改行を意味する\r\nなどが消え、読み書きしやすくなっているのが分かります。

app.js
const express = require("express");
const app = express();
app.set("view engine", "ejs");

app.get("/", (req, res) => {
  const text = "Hello World";
  res.render("index", {text});
});
app.listen(3000);
index.ejs
<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8'>
    <title>hoge</title>
  </head>
  <body>
    <p><%= text %></p>
  </body>
</html>

実行の準備

さて、まずは環境の構築を行います。
Node.js, MariaDB(MySQL)はインストールされているものとします。

サーバーサイドの準備

以下のコマンドを順に実行して、フレームワークやミドルウェアを導入します。

  • $ npm init
  • $ npm install express --save,
  • $ npm install mysql
  • $ npm install ejs -- save

MariaDB(MySQL)の準備

以下のSQLを順に実行します。
この操作により、Webサイトの名前・URLに関するテーブルを作成し、データの追加も行います。

  • create database website
  • create table website(name varchar(255), url varchar(255));
  • insert into website(name, url) values ("google", "https://www.google.com/"), ("amazon", "https://www.amazon.co.jp/"), ("apple", "https://www.apple.com/"), ("facebook", "https://www.facebook.com/");

実装する

次のような順序で、簡単なことから実装していきます。

  1. EJSを使い、Webブラウザ上でHello Worldする
  2. DB接続を行いデータを取り出し、ターミナル上にデータを出力する
  3. DB接続を行い、EJSファイルを利用してWebブラウザ上にデータを出力する(ここでは一度失敗してみる)
  4. DB接続を行い、EJSファイルを利用してWebブラウザ上にデータを出力する(再チャレンジし、成功する)

1. EJSでHello Worldしてみよう

まずはDBのことは一旦 忘れて、EJSを使ってHello Worldをしてみます。

下記ソースコードを保存後、ターミナル上で$ node app.jsと入力し、
Webブラウザでhttp://localhost:3000/にアクセスします。

次の画像のように表示されたら成功です。
ちなみに、
app.jsのソースコード内のapp.get()の第一引数はリクエストURL、
第二引数はリクエストが送られたときに実行されるコールバック関数を指します。

この処理を口語的に説明するなら、
http://localhost:3000/が呼ばれたら次のコールバック関数を実行してね!
そしてそのコールバック関数には、index.ejsを表示して!っていう命令も含まれてるよ!
といったところでしょうか。

image.png

実行するソースコード

app.js
const express = require("express");
const app = express();
app.set("view engine", "ejs");

app.get("/", (req, res)=>{
    res.render("index.ejs");  // デフォルトでは /viewsからの相対パスで表すので注意
})

app.listen(3000);

views/index.ejs
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Hello World</h1>
  </body>
</html>

2. DBに接続してみよう

次に、DB内のデータをターミナル上で表示する実装を行います。

app.jsを次のように書き換えます。
なお、mysql.createConnection()の各種ユーザ情報は、必要に応じて書きかえてください。

実行するソースコード

app.js
const express = require("express");
const app = express();
const mysql = require("mysql");
const connection = mysql.createConnection({
    host: "localhost",
    user: "root",
    password: "1234",
    database: "website"
});
app.set("view engine", "ejs");

app.get("/", (req, res)=>{
    const sql = "select * from website";
    connection.query(sql, (err, result, fields)=>{
        if(err)throw err;
        console.log(result);
    })
    res.render("index.ejs");  // デフォルトでは "/views"ディレクトリからの相対パスで表すので注意
})

app.listen(3000);

ターミナル上で$ node app.jsと入力し、Webブラウザでhttp://localhost:3000/にアクセスします。
前回と同様にHello WorldがWebブラウザに表示されており、
更にターミナル上に、次のような表示があれば成功です。

ここではconsole.log(result)の実行による、ターミナルのデータ出力結果から、
変数resultにはDBから取り出したデータが入っていることが確認できます。

$ node app.js
[
  RowDataPacket { name: 'google', url: 'https://www.google.com/' },
  RowDataPacket { name: 'amazon', url: 'https://www.amazon.co.jp/' },
  RowDataPacket { name: 'apple', url: 'https://www.apple.com/' },
  RowDataPacket { name: 'facebook', url: 'https://www.facebook.com/' }
]

3. DB内のデータをWebブラウザに表示してみる

『1.EJSでHello Worldしてみよう』, 『2.DBに接続してみよう』では、
res.render("index.ejs")とレンダリング先を表記し、ルーティングを設定していました。

今回はレンダリング先にデータを渡すために、
app.get()内にレンダリング先だけではなく、DBから取得したデータも記述する必要があります。
そこでres.render()の第二引数に、レンダリング先に送るデータを記述します。

より具体的にはいえば、
『2. DBに接続してみよう』で、変数resultにDBのデータが格納されることが確認できていました。
このresultをres.render()の第二引数に指定します。
従って、ここではres.render("index", { web: result})と記述します。

ところで{ web: result}という記述に、ややこしさを感じるかもしれません。
これはresultからwebへ名前を置換してから、データを送るという処理を含んでいます。

EJSに対して、resultという変数名をそのままに渡してしまうと、
「result?結果?いや何の結果を表す変数なのか、なんのこっちゃわからん」と、
フロントエンドエンジニアが困惑することになってしまいます。

(webという変数名ならば適切なのかという問題はさておき。)

実行するソースコード

DBに保存しているWebサイト名やURLをWebブラウザ上に出力するため、
app.jsindex.ejsを、それぞれ次のように書き換えます。

app.js
const express = require("express");
const app = express();
const mysql = require("mysql");
const connection = mysql.createConnection({
    host: "localhost",
    user: "root",
    password: "1234",
    database: "website"
});
app.set("view engine", "ejs");

app.get("/", (req, res)=>{
    const sql = "select * from website";
    connection.query(sql, (err, result, fields)=>{
        if(err)throw err;
        console.log(result);
        res.render("index", { web: result});
    })
})

app.listen(3000);
/views/index.ejs
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Hello World</h1>
    <%= web %>
  </body>
</html>

ターミナルにnode app.jsを入力し、Webブラウザでhttp://localhost:3000/にアクセスします。
次の画像のような、Hello WorldとObjectという文字の羅列が確認できるでしょうか?
半分だけ成功です!

image.png

本来であればGoogleAppleといったWebサイト名や、
https://www.google.com/のようなURLがほしかったところですが、
DBから何らかのデータを取り出すことには、ひとまず成功したのではないでしょうか。
[]で囲まれたよくわからないものは4つで、DBに登録したレコードもちょうど4つでしたしね。

次の項で、この問題を解決します。

4. DB内のデータをWebブラウザに表示してみる(再挑戦)

前項ではDBから、どうやら何らかのデータを取り出すことには成功しましたが、
Webサイトの名前やURLを取得することはできませんでした。

この原因について考えます。

http://localhost:3000/にアクセスしたとき、
console.log(result)の実行によって、ターミナルに何か表示されたことは覚えているでしょうか?

それは、次のような内容でした。

$ node app.js
[
  RowDataPacket { name: 'google', url: 'https://www.google.com/' },
  RowDataPacket { name: 'amazon', url: 'https://www.amazon.co.jp/' },
  RowDataPacket { name: 'apple', url: 'https://www.apple.com/' },
  RowDataPacket { name: 'facebook', url: 'https://www.facebook.com/' }
]

一見するとオブジェクトですが、
出力されたデータは[]で囲まれているため、これはオブジェクトたちを格納している『配列』です

従って、例えばgoogleを取得する場合はweb[0]["name"]と記述します。

実行するソースコード

例として、googlehttps://www.google.com/をWebブラウザ上に表示してみます。
次のようにindex.ejsを書き換え、再度 $ node app.jsで実行します。
下記画像のように、Webサイト名とURLが表示されたら、成功です。

/views/index.ejs
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Hello World</h1>
    <%= web[0]["name"] %>
    <br>
    <%= web[0]["url"] %>
  </body>
</html>

image.png

この他にも応用として、
for文を利用するなどして、一度に複数のデータを出力すること、
あるいはSQLを変更してデータの更新・追加・削除することも可能です。

おわりに

プログラミングを本格的に始めて1年も満たない未熟者の言葉ではありますが、
プログラミング言語の文法を修めるだけでは、Webのシステム開発は正直 不可能です。

私はJavaScriptの文法を学習した後、サーバーサイド言語としてのJavaScript(Node.js)に入門したのですが、
すぐにHTTPリクエスト、ルーティングなどといったWeb特有の専門用語に悩まされました。

Webアプリケーションのサーバーサイドへ入門する前に、
『この一冊で全部わかるWeb技術の基本』, 『Webを支える技術』, 『Web技術速習テキスト』といったWeb周りの情報に触れておくことを、強くおすすめします。

参考

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

Material-ui インストールコマンド

Material-uiのインストールコマンド

下記コマンドを実行

npm install --save @material-ui/icons @material-ui/core @material-ui/system @material-ui/styles

意味

@matesial-ui/core→Material-uiを入手する
@material-ui/icons→公式マテリアルアイコンを使用できる
@material-ui/system→"style functions" と呼ばれる低レベルのユーティリティ関数を提供し、強力な設計システムを構築する
@material-ui/styles→Material-UIコンポーネントを使用していないReactアプリケーションのスタイルを設定する

公式ドキュメントはこちら
https://material-ui.com/ja/

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

next.js heroku デプロイ Error: Couldn't find that app.

next.jsをherokuにデプロイする日本語記事が見当たらなかったため投稿しました。

環境:
node.js
react
next.js

herokuへpushまでは省略します。
push後urlへアクセスすると拒絶されたところから
ログを確認してから出直せと言われたので、

heroku logs -tail
Error: Couldn't find that app.
 ›
 ›   Error ID: not_found

で、結論から
https://github.com/mars/heroku-nextjs
を参考にしました。

package.json
{
  "name": "next-yaninavi",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "next",
    "build": "next build",
    "start": "next start -p $PORT"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "next": "^10.0.5",
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
  }
}
npm run build
NODE_EV=production npm run start

heroku触ったことなかったのでビクビクしてたけど、ngrokみたいな感じかな
恐らく、localhostとurlを繋いでるけどそのルート記述が間違ってたのかな。。

理解したら追記します!

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