- 投稿日:2021-01-17T21:15:41+09:00
【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以上
- 投稿日:2021-01-17T18:47:20+09:00
[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 管理コンソールでドメイン全体の委任を設定した際に表示されます。
サービスアカウントの準備
API とサービス
>認証情報
サービスアカウントの準備手順
サービスアカウント作成
+ 認証情報の作成
からサービス アカウント
を選択。
下記のとおり設定して「完了」
- サービス アカウント名:適当に(ここでは domain-delegation)
- サービス アカウントID:自動設定
- サービス アカウントの説明:空ないし任意の説明文
ドメイン全体の委任を有効化
ドメイン全体の委任の表示
をクリックして展開。
G Suite ドメイン全体の委任を有効にする
にチェックを入れて「保存」
これにより「OAuth 2.0 クライアント ID」が表示されたことを確認し、再度サービスアカウントの編集ボタンをクリック。
鍵を追加
プルダウンから新しい鍵を作成
をクリック。
キーのタイプとして「JSON」を選択して「作成」し、秘密鍵Jsonファイルをダウンロード。
このファイルがソースコード上でrequire('./credentials.json')
により取り込んでいた json ファイル。
ドメイン全体の委任の承認
Google 管理コンソール >
セキュリティ
おわりに
探し方が悪いのかサービスアカウント認証関連の公式ドキュメントを探しても
subject
を指定しなきゃダメという情報を見つけられず、参考にさせて頂いた記事を見てようやく認証を通すことができました。
一応subject
の指定方法は適切な形になっているはずですし、async/await 版でのサンプルにもなっていると思いますので参考になれば。参考
- 投稿日:2021-01-17T18:05:27+09:00
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 を利用して変換するのが簡単です
実行環境の準備
- 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ストリーミングだと送信ごとに毎回署名してるようなので効率は落ちるのでしょうかね
- 投稿日:2021-01-17T17:07:57+09:00
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-graphql2. ライブラリのインストール
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 husky3. サンプルデータの用意
/data/users.js
を追加して、DBの代わりに使用した。
(内容はサンプルソースコードのusers.jsを参照)4. schemaを定義
/schema/schema.graphql
を作成してschemaを定義。
今回は参照系クエリのみ実装した。
データ構造は、ユーザとそれに紐づく投稿といった形。schema.graphqlconst { 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.jsconst { 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. [参考] コンテナ化
Dockerfile
とdocker-compose.yml
を追加。FROM node:14.15.4-slim WORKDIR /usr/local/src/express-graphql ADD . . RUN yarn install CMD [ "yarn", "start" ]docker-compose.ymlversion: '3' services: graphql: build: . container_name: graphql-container ports: - "3000:3000"8. Expressの起動
起動コマンドを実行。
# コンテナで実行 $ docker-compose up # コンテナを使用せず実行 $ yarn start9. クエリを実行
http://localhost:3000/graphql をブラウザで開く。
【サンプルクエリ】
内容としては、ユーザ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" } ] } } }参考
- 投稿日:2021-01-17T16:07:45+09:00
Azure IoT Hub に Node.js で publish
Ubuntu 20.10 で確認しました。
ライブラリーのインストール
sudo npm install -g azure-iot-device-mqtt sudo npm install -g azure-iot-deviceazure_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 ("*** 終了 ***") // ---------------------------------------------------------------.envCONNECTSTRING="HostName=iot-aa.azure-devices.net;DeviceId=pansy;SharedAccessKey=SvsFWVsCy2bJkH0QuPJeIabcdefgh8mo6S6vNCjom92="次のコマンドでも調べることが出来ます。
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
- 投稿日:2021-01-17T15:57:20+09:00
【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の理解が重要。
- 投稿日:2021-01-17T15:57:20+09:00
【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の理解が重要。
- 投稿日:2021-01-17T13:49:47+09:00
Node.js で車とシリアル通信する
先日車が壊れたのを機に車について調べていたら OBD (On-board diagnostics) という診断機能を知りました。大抵の車には OBD のコネクタが搭載されていて、これに ELM327というマイコンを使った製品をつなぐと Bluetooth のシリアル通信などで簡単に情報をやりとりできるそうです。おもしろそうなので試してみました。
重要 このポストと同じ方法で車の診断ログをリセットしたりすることもできます。診断ログが正しくない状態になると、修理の適切なタイミングなどがわからなくなるはずです。コマンドを車に送る前によく考えた上で自己責任で試してください。
ELM327 のドングルを手に入れる
これらの製品は Amazon 等で廉価に入手できるようです。全製品で固定っぽい Bluetooth の接続パスワードが書かれていたので念のため黒塗りにしました。
車のOBDポートを探す
運転席のどこかにある場合が多いようです。私の車はここにありました。
ここに購入した上記のドングル型デバイスを接続します。
通信方法を確認
送信
AT コマンドというものを送るとマイコンを操作することができるようです。今回は以下の2つのコマンドを使いました。
AT Z
: 状態をリセットするAT SP 0
: OBDの通信プロトコルを自動で検出通信プロトコルは様々なものがあるようですが、私の車で試した限りでは
AT SP 0
の自動検出でうまく通信できました。AT コマンドの他に OBD コマンドというものがあり、これで様々な OBD の情報を取得できました。Wikipedia にコマンドのリストがあったため、これを参照しました。
OBD コマンドは
Mode
とPID
から構成されています。現在のデータを読み取る01
というモードを今回は使いました。取得できるデータごとにPID
が振られており、例えばPID
が0C
のエンジン回転数の情報を読み取る 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 サーバー
- クライアントを表示する ウェブサーバー
また、エンジン回転数以外にも現在の運転速度と冷却水温度も取得してみました。
動画を撮りながら走ってみると、いい感じにデータが取れているようです✌️
上記のコードは以下のリポジトリにあります。
https://github.com/kamataryo/parse-obd-serial
- 投稿日:2021-01-17T12:51:06+09:00
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.jsconst { 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接続がデフォルトに変更されたのか?
参考文献が見つからず原因がわかる方がいらっしゃいましたら、ご教授ください。
- 投稿日:2021-01-17T11:48:30+09:00
M5Core2のLCDにWebページのスクリーンショットを表示する
M5Core2のLCDにいろんな情報を表示する際に、画面レイアウトを試行錯誤しながらコンパイル・書き込み・実行を繰り返すのは手間なので、HTMLで画面を作成してそのスクリーンショットをM5Core2のLCDに表示するようにします。
もろもろのソースコードを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.jsvar browser = await puppeteer.launch(); var page = await browser.newPage();この部分で、ブラウザの表示サイズを変更しています。
api/controllers/screenshot/index.jsawait 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.jsawait 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.jsvar buffer = await page.screenshot({ type: type });JpegかPNGが選べます。
最後に、ブラウザを閉じて終わりです
api/controllers/screenshot/index.jsbrowser.close();この画像バイナリを呼び出し元に返します。
api/helpers/binresponse.jsclass 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.jsvar 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.cppconst 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.2M5Core2に表示するWebページの作成
HTMLはもちろん、CSSやJavascriptも使えますので、通常のWebページ作成時の要領と同じです。
ただし、小さい解像度である320x240では表示が崩れてしまう可能性大なので、Chromeブラウザ上で試行錯誤したいところ。
Chromeの開発ツールを使えばよいです。F12キーを押して開発ツールを表示させ、Elementsタブの左にある□2つのアイコンをクリックします。
次に、以下の部分でResponsiveが選択された状態にすると、表示領域の縦横の解像度を変更できるようになるので、ここで320x240とします。これで、Webページのレイアウト作成がやりやすくなるかと思います。
完成したら、同じURLをM5Core2からNode.jsサーバに渡すとChromeの開発ツールで見えている状態のままのJpeg画像が取得され表示されます。以上
- 投稿日:2021-01-17T04:54:58+09:00
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.jsconst 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.jsconst 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/");
実装する
次のような順序で、簡単なことから実装していきます。
- EJSを使い、Webブラウザ上でHello Worldする
- DB接続を行いデータを取り出し、ターミナル上にデータを出力する
- DB接続を行い、EJSファイルを利用してWebブラウザ上にデータを出力する(ここでは一度失敗してみる)
- 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を表示して!っていう命令も含まれてるよ!
といったところでしょうか。実行するソースコード
app.jsconst 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.jsconst 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.js
とindex.ejs
を、それぞれ次のように書き換えます。app.jsconst 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という文字の羅列が確認できるでしょうか?
半分だけ成功です!本来であれば
Apple
といった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/' } ]一見するとオブジェクトですが、
出力されたデータは[]
で囲まれているため、これはオブジェクトたちを格納している『配列』です。従って、例えば
web[0]["name"]
と記述します。実行するソースコード
例として、
https://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>この他にも応用として、
for文を利用するなどして、一度に複数のデータを出力すること、
あるいはSQLを変更してデータの更新・追加・削除することも可能です。おわりに
プログラミングを本格的に始めて1年も満たない未熟者の言葉ではありますが、
プログラミング言語の文法を修めるだけでは、Webのシステム開発は正直 不可能です。私はJavaScriptの文法を学習した後、サーバーサイド言語としてのJavaScript(Node.js)に入門したのですが、
すぐにHTTPリクエスト、ルーティングなどといったWeb特有の専門用語に悩まされました。Webアプリケーションのサーバーサイドへ入門する前に、
『この一冊で全部わかるWeb技術の基本』, 『Webを支える技術』, 『Web技術速習テキスト』といったWeb周りの情報に触れておくことを、強くおすすめします。参考
- 投稿日:2021-01-17T03:19:08+09:00
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/
- 投稿日:2021-01-17T00:21:34+09:00
next.js heroku デプロイ Error: Couldn't find that app.
next.jsをherokuにデプロイする日本語記事が見当たらなかったため投稿しました。
環境:
node.js
react
next.jsherokuへ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 startheroku触ったことなかったのでビクビクしてたけど、ngrokみたいな感じかな
恐らく、localhostとurlを繋いでるけどそのルート記述が間違ってたのかな。。理解したら追記します!