20210502のNode.jsに関する記事は7件です。

Node.js基本編 Express+SQLiteで超定番のTo Doメモアプリを作る

本編はNode.js+Express+SQLiteを使って、DataBaseにCRUDできるアプリを作る工程を紹介をします。実際に作るのは定番中の定番、To Doメモアプリですので難しい事は一切しない初学者向けです。 掌田津耶乃 著のNode.js超入門がベースです。 一応念のため、CRUDとは Create(登録) 新しいデータを登録する。 Read(参照) レコードをテーブルから取り出す。 Update(更新) レコードの内容を更新する。 Delete(削除) レコードを削除する。 一応念のため、Webアプリケーションとは(サイトとの違い) Webサイト=情報を提供 静的ファイルをホスティングしてるだけで、誰が見ても同じ情報を表示する。 Webアプリケーション=情報のやり取り ユーザーがフォーム入力、送信、など様々な行動をアプリケーション上で行える。ユーザーが送信したデータを処理して結果を表示する。 では実際にやっていきましょう。大きく分けて4ステップで説明していきます。 Step1:Node.jsアプリ基盤構築 Step2:DataBase構築 Step3:ページを用意 Step4:CRUD処理を実装 前提条件:Node.jsはインストールしておいてください。インストールの仕方はこちらを参考 Step1:Node.jsアプリ基盤構築 入門編でも紹介したアプリケーションのひな形を生成してくれる便利なツールのExpress-Generatorをインストールしましょう。 npm install -g express-generator これでグローバルにインストールされてシステム全体でExpress-Generatorが使えます。 ではアプリを作っていきます。 express -e アプリ名 express -e todo-app これで「todo-app」という名前のディレクトリができ、各種フォルダも自動で作られます。 Express-Generatorはデフォルトで各種パッケージを入れておいてくれるので、プロジェクトディレクトリに入り、npm installをして全部インストールしましょう。 cd todo-app npm install そして、今回DataBaseを扱う為に必要なSQLite3は、別途インストールが必要です。 npm install --save sqlite3 各ディレクトリの説明をざっくりと、 bin/ アプリを実行する為のコマンドとなるファイルが保管されてます。基本的に触りません。 public/ CSSや画像などをこの中に置きます。 routes/ このディレクトリではルート(URL)ごとの処理がまとめらています。 用意するページのアドレスごとにここにファイルを追加します。 views/ 画面側を作る上で必要なViewファイルを置きます。サーバーサイドからこのファイルに対して値を渡すことが出来ます。 node_modules/ npm installするとパッケージ類はこのディレクトリに入ってきます。基本的に触りません。 app.js これがメインプログラムです。Expressの設定周りを担っています。 これでアプリの基盤はひとまず完成です。Express-Generatorを使うとすごい楽ですね。 Step2:DataBase構築 続いてDataBaseの構築です。DB Browser for SQLiteを使います。 *DB Browser for SQLiteはSQLiteのデータベースをGUIで管理することができるツールです。 導入方法はこちらを参考にしてください、簡単です。 まずDataBaseファイルを作ります。 「新しいデータベース」をクリックします ファイル名は「memo_data」として、保存先はtodo-appフォルダ直下です。これでmemo_data.db / memo_data.sqlite3というファイルが作られます。 次にテーブルを作ります。 今回は超シンプルなメモアプリなので、メモを格納するテーブル(テーブル名:memos)を一つだけ用意ます。 次にカラムを作ります カラムは以下2点に留めておきます。 ・id -これは編集、削除するのに指定する為に必須 ・text (もっとあっていいですが今回はミニマムな実装に留めます) 最初に何個かダミーデータを登録しておきましょう。 普通のアプリ開発では必要なデータを洗い出して、テーブル、カラムをどう分けるかなどをこんな風に図に起こして 設計 するのが当たり前ですが、 今回は超絶シンプルなメモアプリなので設計なんて大そうな作業は不要です。 ちなみにテーブル、カラムなど超基本のデータベース用語ですが、カラムが列(項目)で、テーブルがそれをまとめる表のイメージです。 データベース構築はこれで完了です。 Step3:ページを用意 続いてクライアント側の画面であるページを作っていきます。 Node.jsではテンプレートエンジンというクライアント側の画面が簡単に作成できるパッケージがあります。Express-Generatorで既にejsというテンプレートエンジンがインストール済みなので、これを使ってページを作っていきます。 今回必要なページは4つです。views配下にmemoという名前でフォルダを作り、 メモ一覧表示ページ: index.ejs 新規メモ追加ページ: add.ejs メモ編集ページ: edit.ejs メモ削除ページ: delete.ejs を作成してください、中身のコードは以下になります。 index.ejs <!DOCTYPE html> <html> <head> <title><%= title %></title> </head> <body> <h3><%= title %> <a href="/memo/add">追加</a></h3> <div> <table> <% for(var i in content) { %> <tr> <% var obj = content[i]; %> <td><%= obj.text %></td> <td><a href="/memo/edit?id=<%= obj.id %>">更新</a></td> <td><a href="/memo/delete?id=<%= obj.id %>">削除</a></td> </tr> <% } %> </table> </div> </body> </html> add.ejs(index.ejsとbodyタグ以外は同じ) <body> <h3><%= title %></h3> <p><%= content %></p> <div> <form action="/memo/add" method="post"> <p>メモ内容:<input type="text" name="text"></p> <input type="submit" value="追加"> </form> </div> </body> edit.ejs(index.ejsとbodyタグ以外は同じ) <body> <h3><%= title %></h3> <p><%= content %></p> <div> <form action="/memo/edit" method="post"> <p>メモ内容:<input type="text" name="text" value="<%= memoData.text %>"></p> <input type="hidden" name="id" value="<%= memoData.id %>"> <p><input type="submit" value="更新"></p> </form> </div> </body> delete.ejs(index.ejsとbodyタグ以外は同じ) <body> <h3><%= title %></h3> <p><%= content %></p> <div> <p>メモ内容:<%= memoData.text %></p> <form action="/memo/delete" method="post"> <input type="hidden" name="id" value="<%= memoData.id %>"> <p><input type="submit" value="削除"></p> </form> </div> </body> 見ての通り基本はHTMLファイルで、値を受け取ってそれを表示したり、値を送ったりしているだけです。 title, content, memoDataという名前で値を送る処理はroutes(サーバー側)から行ってます。 Step4:CRUD処理を実装 最後に肝心のCRUD処理を実装していきます。 routes配下にmemo.jsというファイルを作り、以下の処理を書いていきます。 各所にコメントを入れてあります。 var express = require('express'); var router = express.Router(); var sqlite3 = require('sqlite3'); //データベースオブジェクトの取得 const db = new sqlite3.Database('memo_data.sqlite3'); router.get('/', function(req, res, next) { db.serialize(() => { //SQL文, memosテーブルから全てのレコードを取得する(* は全て) db.all("select * from memos", (err, rows) => { if (!err) { const data = { title: 'To Do メモ 一覧表示', content: rows //DataBaseから返された全レコードがrowsに配列で入ります } //viewファイルのmemo/indexにdataオブジェクトが渡されます //res.render(テンプレートファイル名, { 渡す値をオブジェクトで }) → テンプレートファイルを描画する res.render('memo/index', data); } }) }) }); router.get('/add', function(req, res, next) { const data = { title: '追加', content: '新しいデータを入力してください' } res.render('memo/add', data); }); router.post('/add', function(req, res, next) { const tx = req.body.text; //SQL文, DataBaseのレコード作成 db.run('insert into memos (text) values (?)', tx) //res.redirect() 引数に指定したアドレスにリダイレクト res.redirect('/memo'); }); router.get('/edit', function(req, res, next) { const id = req.query.id; db.serialize(() => { const q = "select * from memos where id = ?"; db.get(q, [id], (err, row) => { if (!err) { const data = { title: '更新', content: 'id = ' + id + 'のレコードを更新', memoData: row } res.render('memo/edit', data); } }) }) }); router.post('/edit', function(req, res, next) { //POST送信された値はreq.body内にまとまられている const id = req.body.id; const tx = req.body.text; const q = "update memos set text = ? where id = ?"; db.run(q, tx, id); res.redirect('/memo'); }); router.get('/delete', function(req, res, next) { const id = req.query.id; db.serialize(() => { const q = "select * from memos where id = ?"; db.get(q, [id], (err, row) => { if (!err) { const data = { title: '削除', content: 'id = ' + id + 'のメモを削除しますか?', memoData: row } res.render('memo/delete', data); } }) }) }); router.post('/delete', function(req, res, next) { const id = req.body.id; const q = "delete from memos where id = ?"; db.run(q, id); res.redirect('/memo'); }); module.exports = router; そして、app.jsに以下を追記してください var memoRouter = require('./routes/memo'); app.use('/memo', memoRouter); ここまでできたらnpm startを実行して、http://localhost:3000/memoを開いてください。 最初に登録したダミーデータが表示されていれば一覧表示はうまくいってます。 追加、更新、削除も機能していればバッチリです。 なぜサーバーサイドが必要? ローカルストレージ機能を使えば一時保存できるので、簡易的なメモアプリであればフロントだけで完結できます。しかし、大きすぎるデータや、DataBaseにきちんと保存するのであればサーバーサイドが必要になります。(ブラウザからDataBaseにアクセスできないから) それ以外の理由ではフロントではできない複雑な処理などをサーバー側でやってもらうと言う理由があります。 まとめ テーブル1個のこんなシンプルなアプリなんて現場ではまずあり得ないと思います(せめて複数テーブルの結合innerjoin, outerjoinくらいはやっておくべきでしょうか)が、Node.js、サーバーサイドに徐々に慣れてきました。 次はREST APIに関しての記事を書こうと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.jsとNext.jsについて

React.jsとNext.jsについて勉強しているので、その情報を共有するために投稿します。 solidityとスマートコントラクトを勉強していたが、React.jsと組み合わせてアプリケーションを作成している例をたくさん見かけたこともあり、 React.jsもこの際理解しようということにしました。 違いは下記の通り。 React.js: SPAを考えて作成されている。HTMLをJavaScriptの中に記述できるJSX機能が強力! Next.js:React.jsに各種ライブラリを統合してパッケージ化したもの。React.jsを拡張させることができる! ソースコードは、下記GitHubで公開中 react_app next_app どちらも npx コマンドを利用することで土台部分を自動的に作成してくれるため、すぐに開発に入ることができるという強みを持っている! 要素をコンポーネント化することで、複雑な画面でもプラモデルもパーツを組み立てる感覚で開発できるところが面白いし、扱いやすいと感じた。 エンタープライズ向けのWebページでもおそらく課題になるであろう画面レイアウトの統一とも相性がものすごく良いとも考えている。 ただ、従来のHTMLやJSPと比べてかなり書き方が変わるため、いきなり導入するということはかなりハードルが高そうだが、 かなり柔軟に開発できるためメリットの方が大きいのではないかと考えている。 要素をコンポーネント化することで、複雑な画面でもプラモデルのパーツを組み立てる感覚で開発できるところが面白いし、扱いやすいと感じた。 エンタープライズ向けのWebページでもおそらく課題になるであろう画面レイアウトの統一とも相性がものすごく良いとも考えている。 ただ、従来のHTMLやJSPと比べてかなり書き方が変わるため、いきなり導入するということはハードルが高そうだが、柔軟に開発できるため導入するメリットの方が大きいのではないかと考えている。(特にNext.jsについては、HTMLファイルが無くなり、全てJavaScriptでの記述となるため従来のやり方に慣れている方からには、少なからず抵抗があるかもしれません。。。) ※現在、勉強中のため、適宜追記していきたいと考えています。 以下、参考にした書籍となります。 実践スマートコントラクト開発 node.js超入門 React.js&Next.js超入門
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モナコインで投げ銭できる機能をつけたQiitaみたいなサービス開発してみた

モナレッジというサービスを開発しました モナレッジはqiitaやzennのようにmarkdownで記事を書いて投稿できるサービスです。 一番の特徴は、良い記事を書いた著者に対してMONACOINを投げつけることができることです。将来的にはmonapartyを使ったトークンとかも扱えるようにしたいと思っています。 開発経緯 qiitaで記事を書いてくださっている皆さんの情報発信のありがたさは常日頃感じていますが、その感謝を送る方法として「いいね」や「シェア」くらいしかできないことにもどかしさを感じていました。 インターネット上でお金を扱う方法として、bitcoinに代表されるような、公開鍵暗号の方式を利用したUTXO構造でお金を動かすというのは結構よくできた仕組みだなぁと思っています(もちろん、すべて自己責任なのでGOXとかしたときとかに何もすることができないデメリットもありますが)。 とりわけ、投げ銭などの使い道っていうことだと、モナコインとかの方がクレカ登録して投げ銭とかよりもお気軽だし、有事の際のリスクも低いんじゃないのかなぁとおもっています。 ちょっと動かす用のウォレットを作成して(これはめっちゃ簡単にできる。BIP32以降の仕組みによって、リカバリーフレーズさえちゃんと管理すれば、複数の秘密鍵の管理もそんなに苦ではない)、そこに数MONA入れたじょうたいで投げ銭をするというのも良いだろうし、 まだMONAを持っていない人は、bitbankとかの取引所で購入するか、記事を書いてMONAをもらったりするのが良いかも。 開発の思い 僕が、いわゆる暗号資産にかんするところで違和感を感じているのが、投機の対象にばかり注目が集まりがちなところです。 まあ、ボラティリティが大きくなるような仕組みなのでその手のものが好きな人は好きなんでしょうが、個人的にはこの技術を使ってどんな新しいサービスができるのかなぁというところの方が気になるところではあります。 このサービスではサインアップの際に、パスワードが必要ありません。どうしてそんなことができるかというとデジタル署名を利用しているからです。 このサービスではログインをする代わりに、記事の投稿時などユーザの識別が必要なタイミングではデジタル署名を求めるようにしています。 デジタル署名に関してわからない方は、これを機に公開鍵暗号などと合わせて一度勉強してみてください。暗号資産を扱う上で知っておいた方がいいと思います。 ユーザ認証のタイミングで署名を求め、バックエンドで署名を検証することでユーザを認証する仕組みをとっています。 そんなこんなでweb3.0っぽいけど、おそらくピュアな3.0じゃないので2.5くらいなのかな? 機能 記事を書く人はmdで記事を書けます 読んだ人は、良いなと思った記事に投げ銭をすることができます。 開発技術 ここからはエンジニアが興味ありそうな部分 フロント : Vue, Nuxt バックエンド : Node.js, Express, postgres 基本的には、フロントで署名を作成し、バックエンドでリクエストの検証をデジタル署名の検証で行うという流れになります。 なので、いわゆる一般的なwebサービスなどとは一味違ったUXになっていると思います。 その辺は使って感じてみてください 大事なのは, mpurseという拡張機能。 Ethereumのmetamaskに相当するものですね。 これが、送金、署名作成(厳密にはこれが送金の大部分でもあるんだけど、そこは割愛)などをフロントのロジックでやってくれます。 これにより、バックエンド側にユーザの機密情報(カード情報、パスワード、etc)を持たずとも、ユーザを認証し、送金機能を持たせることが可能になります。 これは開発者にとってはかなり嬉しいことで、 お金とユーザ情報を扱うサービスなのに、サーバ側にその情報を保存しておかなくて良いので、とても開発しやすいです。 これは、公開鍵暗号の仕組みがベースになっていて、 暗号を利用したアプリケーションの台頭で、クライアントが秘密鍵を持ち始める動きが進んことで、なし得ることができる設計です。 まだまだ、この分野は開拓途中なので、みんなで盛り上がって 楽しんでいきましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js】Express.js / MySQL / ejs / を使ったDBのCRUDアプリ

目的 現在、MySQLに保管した店舗データを編集できるアプリケーションを作成しています。Node.jsのexpress.jsを使用し、viewにはejsを利用しています。この記事では、初学者の私がつまずいた、パラメータの受け渡しについて記述します。 ずばり「企業一覧画面から、企業に属した店舗の一覧を表示させる」処理についてです。 一連の処理は、こんな感じです 企業+店舗一覧を取得(MySQL) → 企業一覧画面で企業名をクリック(ejs) → 店舗一覧画面(ejs) → 店舗名のリンクをクリック (ejs) → ★ express.js(app.js)でデータを加工(MySQL) → ★ 加工したデータをejsに渡す(express.js) → ★ ejsで加工したデータを表示させる 対象読者 ・Express初心者 ・アプリ作成初心者 ・MySQL初心者 環境と周辺構造 ・local (mac Big Sur) ・AWS MySQL 基本となるデータベースとの接続やCRUD処理の基礎的な書き方等については、主にリンク先の記事を参考に作成しました。 参照:Express.js(node.js)からMySQLへの接続とCRUD操作 テーブル構造 テーブルの相関関係は以下の通りです。 account_masterが企業、shop_masterが企業が運営する店舗と仮定し作成しています。 テーブルの関係性を簡単に表すと、 account_master.account_id = shop_master.shop_account_idです。 企業側のaccount_idは店舗側のshop_account_idと同じことを意味しています。 画面イメージ 少し雑ですが、画面のイメージは以下の通りです。 ・企業 一覧画面(account_master_index.ejs) ・店舗一覧画面(shop_index) 企業名リンクをクリックすると、店舗一覧画面に遷移します。 ディレクトリ構造 ※現在、未完成のため一部のみ記載 account_app/ ├── node_modules ├── views │ ├── account_master_index.ejs │ ├── edit.ejs │ └── shop_index.ejs ├── .env ├── .gitignore ├── app.js ├── package.json └── README.md app.jsにexpress.jsやMySQLの処理を記述し、viewsの中でテンプレートを作成しています。 前提 参考サイトでは、簡易なテーブルのデータで作成されていたため、企業の一覧や、店舗の一覧は簡単に表示まで出来ました。しかし、企業に基づく店舗など関係性がちょっと複雑な場合、どう表示させればよいかが全く分かりませんでした。 先に企業一覧について、次に店舗一覧について記述します。 企業一覧画面 app.js 企業一覧画面は参考サイトを元に、テーブル名、select文を変えるだけでOKでした。※記述外の設定は参考サイトを参照ください。 //app.jsの企業一覧に関する部分 app.get('/', (req, res) => { const sql = "select * from account_master where is_deleted = 0; con.query(sql, function (err, result, fields) { if (err) throw err; res.render('account_master_index',{account_master : result}); }); }); app.getは 第一引数に指定したURLに対応しており、express.js のアプリケーションのルート( 今回は、localhost:3000 )へのGETメソッドに対応します。con.queryでselect * from account_master where is_deleted = 0"を取得。 例外処理を記述した後、res.renderの第一引数にデータを表示させたいテンプレートを設定し、第二引数に .ejs に渡す名前を指定します。この命名により、ejs側ではresultではなくaccount_masterを使って取得したデータを利用できます。 account_master_indx.ejs 一部抜粋した記述です。 //account_master_indx.ejsの中身 <% account_master.forEach(function (value) { %> <tr> <td><%= account_id %></td> ★ <td><a href="/shop_index/<%= value.account_id %>"><%= value.account_name %></a></td> <td><%= value.account_email.split('*') %></td> <td><%= value.mail_requested %></td> <td><%= value.is_deleted %></td> <td><%= value.updated_at %></td> <td><%= value.account_form_url %></td> <td><a href="/edit/<%= value.ue_account_id %>">up</a></td> </tr> <% }); %> 参考サイトはforEachを使っていたため、そのまま使っています。<% %>や、<%= %>で処理を書くのか、 HTML として表示させるかを書きます。 ★ここで4行目に注目 <a href="/shop_index/<%= value.account_id %>"><%= value.account_name %></a>で企業 id のパラメータをURLに指定しています。後にこれと同じURLのパラメータとして表示させる処理をreq.paramsを使い app.js 内に で記述します。 ここからが今回の記事の本題です ポイントとしては以下のとおりです。 ・個別に呼び出すのではなく、まず店舗の全データを取得する ・配列を新たに作成する ・for文、if文で条件指定する ・加工した「会社一覧に紐づく店舗一覧の配列を作成するデータ」をejs側で表示する app.jsでデータを加工する 失敗した考え方 店舗一覧画面を出そうした際に、まず企業一覧画面と企業の編集画面を参考にしました。というのも、編集画面は企業のaccount_idを自然に渡せていたからです。そのため、店舗一覧画面も企業一覧と編集画面のようにaccount_idを受け渡せないかと考えました。 そもそも、この↑部分の認識が間違っており、正しくはexpress.js側で処理したデータをejsで表示させる。つまり、ejsからgetメソッドで表示させる場合はejsから値を受け渡すなどは行わないです。 成功した考え方 いきなり企業に紐づく店舗一覧を、expressで記述しそれをejs側で表示させるのではなく、まずapp.js内で先に店舗の全データを出します。そのデータを使って、必要な情報だけを載せた配列を新たに作成します。作成した配列をejs側で呼び出し、表示させます。 店舗一覧画面 店舗一覧画面に関する記述は以下のとおりです。 app.js //店舗データを取得するための変数を定義 var shopDat; //店舗の全データを取得 con.query('select * from shop_master where is_deleted = 0;', function (error, results, fields) { if (error) throw error; shopDat = results; //shopDatに代入 }); ~~(中略)~~ //企業ごとの店舗一覧を取得しshop_index.ejsで表示させるためのデータ加工 app.get('/shop_index/:shop_account_id', (req, res) => { let shops = []; //新たな配列を作成 for(let i = 0; i < shopDat.length; i++){ if (req.params.shop_account_id == shopDat[i].shop_account_id) { //表示するパラメータを指定 var target_shop = { //配列に入れるオブジェクトデータを定義 "shop_id": shopDat[i].shop_id, "shop_name_jp": shopDat[i].shop_name_jp, "shop_name_en": shopDat[i].shop_name_en, "shop_account_id": shopDat[i].shop_account_id, "is_deleted": shopDat[i].is_deleted, "updated_at": shopDat[i].updated_at }; shops.push(target_shop) //空の配列shopsに.pushで追加 } }; res.render('shop_index',{shop_data :shops}); //shop_dataとしてshop_index.ejsに渡す }); 先に shopDat を宣言し、そこにMySQLから店舗の全データを代入します。店舗一覧画面を表示させる箇所に、 shops という新たな空の配列を作成します。更にi番目の shopDat のデータを保持させる target_shop というオブジェクトを作成します。shops.push(target_shop)を shops という空の配列に、 for 文で作成されたオブジェクトを追加して、加工データを作成します。 この時req.params.shop_account_id == shopDat[i].shop_account_idではURLに渡すパラメータと企業に紐づく店舗の情報を一致させるために記述しています。※shop_account_idは企業のアカウント id と同じです。 req.params はリクエストされたパスからパラメータを取得するに使う文字列です。 [Node.js][Express]リクエストからパラメータを取得する・POSTされたデータを取得する shop_index.ejs //店舗一覧画面に関係する部分 <% for (let i = 0; i < shop_data.length; i++) { %> <tr> <td><%= shop_data[i].shop_id %></td> <td><%= shop_data[i].shop_name_jp %></td> <td><%= shop_data[i].shop_name_en %></td> <td><%= shop_data[i].shop_account_id %></td> <td><%= shop_data[i].updated_at %></td> <td><a href="/edit/<%= shop_data[i].shop_id %>">更新する</a></td> </tr> <% }; %> 企業一覧画面ではforEachを使いましたが、こちらは for 文で表記しました。これで、企業に属する店舗の一覧を表示させることができました。 小技とつまずいたポイント ・出力したいデータが正しいかどうかをHTML(.ejs)でみたい → <%- JSON.stringify(shop_data) %>と記述することで見られます! ・JavaScriptのデータは配列[ ]の中にオブジェクト{ }をもつことができる。 → JavaScriptは [{ name:aaa, email:xxx@yyyy}, { name:bbb, email:yyy@xxxx}, { }....]とできるようです。 ・forとif文を一気に記述するのではなく、一つづつ書くこと → 処理を一気に書こうとして、ほしいデータをなかなか出すことが出来ませんでした。落ち着いて出力されたデータを見ながら、一つづつ解決するほうが結果早いですね。 ・配列から要素を取り出す方法 → iをつけることに、なかなか気がつけませんでした。 まとめ JavaScriptを勉強しはじめて3週間ほどですが、MySQLのデータを加工を実施しました!空の配列を作って、ほしいデータを作成をすることは初めての作業でしたが、なんとかうまく出来たので良かったです。途中で、配列なのか連想配列なのか迷ったりしたため中々答えにたどり着けませんでした。これが初学者の方のためになればと思います。 また、一部未完成・不十分な記述がありますので、ご教示いただけると幸いです! 以上 参照 ・Express.js(node.js)からMySQLへの接続とCRUD操作 ・[Node.js][Express]リクエストからパラメータを取得する・POSTされたデータを取得する ・for 文と push メソッドを使って配列要素を複数生成
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Serverless FrameworkでREST APIを作成してみた (Lambda, API Gateway, DynamoDB, Node.js)

はじめに 前回の記事でServerless FrameworkでLambdaの開発環境を構築しました。 引き続きServerless Frameworkの勉強ということで、Lambda(Node.js), API Gateway, DynamoDBの構成でREST APIを作成してみることにしました。 実装 Lambda関数の中身をjsファイル、構築するリソースの設定をserverless.ymlに記述します。 Lambda関数の作成 REST APIを実装するにあたり、データ作成(Create), 全データ取得(List), データ取得(Get), データ更新(Update), データ削除(Delete)の機能が必要です。 そのため、各機能ごとにLambda関数を作成する必要があります。 それぞれの関数を説明すると長くなるので、データ更新用の関数(create.js)のみ説明します。 create.js 'use strict'; const uuid = require('uuid'); const AWS = require('aws-sdk'); const dynamoDb = new AWS.DynamoDB.DocumentClient(); //ドキュメントクライアントを使用してNode.jsでDynamoDBにアクセス module.exports.create = (event, context, callback) => { const timestamp = new Date().getTime(); const data = JSON.parse(event.body); //JSONをJSのオブジェクトに変換 //textがstringじゃないときにエラーを返す(バリデーション) if (typeof data.text !== 'string') { console.error('Validation Failed'); callback(null, { statusCode: 400, headers: { 'Content-Type': 'text/plain' }, body: 'Couldn\'t create the todo item.', }); return; } //テーブル名(TableName)と各カラム(Item)のオブジェクト const params = { TableName: process.env.DYNAMODB_TABLE, Item: { id: uuid.v1(), text: data.text, checked: false, createdAt: timestamp, updatedAt: timestamp, }, }; //DynamoDBへのデータ保存 dynamoDb.put(params, (error) => { //エラー処理 if (error) { console.error(error); callback(null, { statusCode: error.statusCode || 501, headers: { 'Content-Type': 'text/plain' }, body: 'Couldn\'t create the todo item.', }); return; } //処理成功時のレスポンス const response = { statusCode: 200, body: JSON.stringify(params.Item), //JSオブジェクトをJSONに変換 }; callback(null, response); }); }; Node.jsからDynamoDBにアクセスするために、ドキュメントクライアントを利用します。 const dynamoDb = new AWS.DynamoDB.DocumentClient();と定義することで、dynamoDb.get(データ取得)、dynamoDb.put(データ保存)のようにJSでDynamoDBのデータの基本操作を行うことができます。 データ保存を行うためには、テーブル名(TableName)とカラム(Item)のオブジェクト(params)を用意してdynamoDb.put(params, (error)のように引数で渡してあげればOKです。 const params = { TableName: process.env.DYNAMODB_TABLE, Item: { id: uuid.v1(), text: data.text, checked: false, createdAt: timestamp, updatedAt: timestamp, }, }; dynamoDb.put(params, (error)の中身には、データ保存の処理が失敗したときと成功したときのcallbackを記述します。 処理が成功していれば、ステータスコード(statusCode)と保存内容(body)を返します。 //処理成功時のレスポンス const response = { statusCode: 200, body: JSON.stringify(params.Item), //JSオブジェクトをJSONに変換 }; callback(null, response); serverless.ymlの作成 API GatewayやDynamoDBのリソースを構築するために、serverless.ymlを記述します。 API Gatewayの構築 公式ドキュメントを参考に、Lambda関数のトリガーイベントとしてAPIGatewayを作成します。 functions: #Lambdaの構築 create: #関数名を指定 handler: todos/create.create #handler関数の指定(ディレクトリパス.モジュール名) events: #Lambdaのイベントトリガーを指定 - http: #httpイベントをトリガとしてAPI Gatewayを構築 path: todos #リソースの指定 method: post #メソッドの指定 cors: true #cors有効/無効の指定 DynamoDBの構築 DynamoDBの構築についても、公式ドキュメントを参考にします。 resources: #リソースの構築 Resources: TodosDynamoDbTable: #DynamoDBの構築 Type: 'AWS::DynamoDB::Table' DeletionPolicy: Retain Properties: AttributeDefinitions: - AttributeName: id AttributeType: S KeySchema: - AttributeName: id KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 TableName: ${self:provider.environment.DYNAMODB_TABLE} IAM設定 LambdaからDynamoDBにアクセスするための権限を付与するために、ProviderにiamRoleStatementsを追記します。 provider: name: aws runtime: nodejs12.x environment: DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage} iamRoleStatements: - Effect: Allow Action: - dynamodb:Query - dynamodb:Scan - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem Resource: 'arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}' デプロイ 以下のコマンドでデプロイを行います。 sls deploy -v AWSコンソールのLambdaを開くと、作成した関数がデプロイされているのを確認できます。 API Gatewayを開くとdev-serverless-rest-api-with-dynamodbが作成されています。 また、DynamoDBにもテーブルが作成されます。 動作確認 insomniaを使って挙動をみてみます。 エンドポイントはデプロイの際にターミナルに表示されたものを使います。 endpoints: POST - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos GET - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos GET - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos/{id} PUT - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos/{id} DELETE - https://zb6e0z65ka.execute-api.us-east-1.amazonaws.com/dev/todos/{id} 試しにPOSTリクエスト(Create)を飛ばしてみると、200 OKとレスポンスが返ってきます。 また、DynamoDBのテーブルにはデータが追加されています。 おわりに Serverless Framework超便利だね。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nodenvでインストールしたバージョンがなかったときの対応

nodenvでバージョンインストールしたくてコマンド実行 $ nodenv install 14.16.1 そんなバージョンないよと言われた。インストールできるバージョンリストを最新にする必要があるようだ。 コマンド実行時に表示されるコマンドを実行してみる。 $ git -C /Users/kawamurakenji/.anyenv/envs/nodenv/plugins/node-build pull share/node-build/15.8.0 | 14 ++++++++++++++ share/node-build/15.9.0 | 14 ++++++++++++++ share/node-build/16.0.0 | 10 ++++++++++ test/fetch.bats | 1 + 35 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 share/node-build/10.23.3 create mode 100644 share/node-build/10.24.0 create mode 100644 share/node-build/10.24.1 create mode 100644 share/node-build/12.20.2 . . . どうやらリストが最新になったぽいので、インストールできるバージョンを確認する。 $nodenv install --list お目当てのバージョンをみつけた。 14.15.4 14.15.5 14.16.0 14.16.1 15.0.0 15.0.1 15.1.0 . . . これでインストールできた。 $ nodenv install 14.16.1 nodenvを使って、特定のプロジェクトでバージョンを適用する。適用したいディレクトリに移動してからコマンド実行。 ディレクトリ別でnodeバージョンを設定すれば、ディレクトリ移動しただけで自動切り替えされるので便利らしい。 $ nodenv local (バージョン) $ node -v デフォルトのバージョンをしたい場合はこれらしい。 nodenv global (バージョン)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js】並列処理(worker_thread)か並行処理(async)かそれが問題だ。

沢山のディレクトリにある沢山のファイルの一覧を取得する時のお話 あるところに400ディレクトリの中に計52万ファイルがありました。一個一個見ていくのは残りの人生が何年あっても足りません。どうしたものかと悩んだおじいさんはNode.jsなんだからasyncの並行処理が良いねと言いました。でも、おばあさんはworker_threadで並列処理の方が絶対早いよと言うのです。 そこで、おじいさんとおばあさんは、どっちが速いのか競争することにしました。 勝負は10回勝負。並列数(nParallel)と並行数(nConcurrences)を1から51まで変化させた時の全てのディレクトリにある全てのファイルのパスをメインスレッドにメッセージとして返すのに要する時間を計測します。48コアのCPU環境で対象ファイルはネット越しにマウントしているディレクトリにあります。並行数と並列数は1~51までとし、ディレクトリを単位として仕事を分割します。また並行数と並列数の組み合わせのうち、ディレクトリ数(400)を超えるものは無視します。 実装 test.mjs import { Worker, workerData, parentPort, isMainThread } from 'worker_threads' import { fileURLToPath } from 'url'; import fs from 'fs'; import path from 'path'; import async from 'async'; const nParallels = [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51]; const nConcurrences = [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51]; const nDirectory = 400; const nLoop = 10; //このディレクトリ以下に400のディレクトリがあり計52万のファイルが存在しています const targetDir = '/target/directory/'; if (isMainThread) { for (const nParallel of nParallels) { for (const nConcurrence of nConcurrences) { if (nParallel * nConcurrence > nDirectory) { continue; } console.time(nParallel + ' para/ ' + nConcurrence + ' conc'); for (let c = 0; c < nLoop; c++) { const dirents = await fs.promises.readdir(targetDir, { withFileTypes: true }); let directories = await async.reduce(dirents, [], async (memo, dirent) => { if ((dirent.isDirectory())) { memo.push(path.join(targetDir, dirent.name)); } return memo; }); //console.log("total", directories.length, "directories"); let directoryChunks = await async.reduce(directories, [[]], async (memo, directory) => { if (memo[memo.length - 1].length >= nConcurrence) { memo.push([]); } memo[memo.length - 1].push(directory); return memo; }); let nFile = 0; await async.eachOfLimit(directoryChunks, nParallel, async (directoryChunk) => { return new Promise((resolve, reject) => { const worker = new Worker(fileURLToPath(import.meta.url), { workerData: { 'directoryChunk': directoryChunk } }); worker.on('message', (message) => { nFile += message.length; }); worker.on('exit', () => { resolve(); }); }); }); //console.log("total", nFile, "files"); } console.timeEnd(nParallel + ' para/ ' + nConcurrence + ' conc'); } } } else { await parentPort.postMessage( await async.reduce(workerData.directoryChunk, [], async (memo, directory) => { const files = await async.map( await async.filter( await fs.promises.readdir(directory, { withFileTypes: true }), async (dirent) => { return dirent.isFile; } ), async (dirent) => { return path.join(directory, dirent.name); } ) return memo.concat(files); }) ); } 実装解説 実装としては、まずメインスレッドで対象ディレクトリのサブディレクトリを持ってきます。 const dirents = await fs.promises.readdir(targetDir, { withFileTypes: true }); let directories = await async.reduce(dirents, [], async (memo, dirent) => { if ((dirent.isDirectory())) { memo.push(path.join(targetDir, dirent.name)); } return memo; }); このサブディレクトリリストを並列実行数毎のチャンクにします。 let directoryChunks = await async.reduce(directories, [[]], async (memo, directory) => { if (memo[memo.length - 1].length >= nConcurrence) { memo.push([]); } memo[memo.length - 1].push(directory); return memo; }); このチャンク毎にasync.eachLimitを使って、Workerを起動します。asyncLimitは配列を非同期処理してくれるのですが、並行実行数(limit)を設定できる便利な関数です。ここではnParallelを設定してます。この関数は並行実行されますが、この中でWorkerを起動することで、nParallelに設定された数のスレッドが立ち上がり、並列実行されます。 await async.eachOfLimit(directoryChunks, nParallel, async (directoryChunk) => {省略... Workerは割り当てられたチャンク(中には並行実行数分のディレクトリパスを保持)を、並行実行で走査し、各ディレクトリのファイル一覧を得てメインスレッドにメッセージとして通知します。asyncfilterで await async.reduce(workerData.directoryChunk, [], async (memo, directory) => { const dirents = await fs.promises.readdir(directory, { withFileTypes: true }); const files = await async.map( await async.filter(省略... 結果(10回の処理時間の平均) という訳で、実験結果。 縦横軸がそれぞれ並列実行数、並行実行数です。高さ方向に平均処理時間をプロットしていますが、並列数1×並行数1のところが飛びぬけて遅い(約45秒)ので、縦軸最大値は15秒として、全体的な傾向が見えやすいようにしてます。 この図からわかる事は、並列実行数だけ、並行実行数だけを増やしても、絶対的な速さは得られない事がわかります。一方で、どっちが効くかというと、このタスクでは、あまり差異はなさそうです。 以下、あまり意味はないですが、早い順に並べてみました。スレッド数11の並行実行数21が最速です。CPUのコア数が48ですから、そこまで達していない事を見ると、どこか(IO?)がサチってますね。 順位 スレッド数 並行実行数 平均処理時間 (秒) 1 11 21 1.0829 2 16 16 1.0939 3 21 16 1.1118 4 11 26 1.1245 5 11 16 1.1663 6 36 11 1.1678 7 16 11 1.1767 8 6 36 1.1891 9 6 31 1.2187 10 21 11 1.2265 11 11 36 1.2314 12 26 11 1.2712 13 6 26 1.32 14 31 11 1.3881 15 26 6 1.4649 16 6 46 1.4758 17 16 21 1.4839 18 41 6 1.4896 19 6 51 1.5072 20 16 26 1.5093 21 16 6 1.5127 22 51 6 1.6431 23 6 16 1.6807 24 26 16 1.6892 25 6 11 1.6996 26 11 11 1.7192 27 31 6 1.734 28 46 6 1.7847 29 36 6 1.8176 30 11 6 1.9193 31 21 6 1.9321 32 11 31 1.9454 33 6 21 2.0346 34 6 41 2.0399 35 6 6 3.0052 36 36 1 4.0687 37 41 1 4.0759 38 31 1 4.0844 39 46 1 4.3219 40 21 1 4.4535 41 26 1 4.5095 42 51 1 4.7892 43 16 1 5.1709 44 1 41 5.4326 45 1 46 5.6377 46 1 51 5.6491 47 11 1 5.9967 48 1 36 6.0155 49 1 26 6.674 50 1 31 6.7652 51 1 21 7.3284 52 1 16 7.5211 53 6 1 8.9749 54 1 11 9.2236 55 1 6 12.4572 56 1 1 44.0639 結論 結局のところ並列実行も並行実行も大切だという事がわかって、おじいさんとおばあさんは末永く仲良く暮らしましたトサ。 めでたしめでたし。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む