20191230のNode.jsに関する記事は8件です。

年末の断捨離アプリをFirebaseでhostingしてみた話(webアプリが少しずつ良くなっていく過程編)

Firebaseのhostingを使ってReactのSPAアプリをデプロイした話

です。
結論だけ話すとそんな感じです。(domainをとっていないことをお許しください)
https://dan-syari.web.app/#/

年末の短期休暇でアプリを1つ作ろうと考えていて、今回作りました。(開発期間は2日間)

今回作ったアプリについて

  • タイトル: 「Dan Syari」
  • 内容: 来年したい目標を3つ立て、それについて簡単な質問に答えるとその目標の中で重要なことがわかる
  • 技術:
    • React
    • Typescript ☆
    • React Hooks ☆
    • Reduxを使わない状態管理 (Context) ☆
    • Firebase (Hosting) ☆
    • Firestore ☆
    • Material UI ☆
    • SPA ☆

☆がついているものは著者は初めて使ったものです

洗い出して列挙してみると、結構挑戦してたことがわかります

本記事の対象者

  • 意気揚々と壮大なアプリを作ろうとして途中で断念してしまう人への解決方法
  • 最初から設計ばかり気にして、手が動かない人
  • 年末で暇すぎて、ゲームしたり漫画読んでたり、とにかく暇人なあなた!!

本記事で伝えたいこと

  • ちっぽけなところから初めて、少しずつ大きくしていくほうが楽しい
  • どんなアプリもデプロイしないと始まらない
  • 作っていくうちに設計のミスに気付いて後悔するけど悪いことばかりではない
    • 個人の失敗は、個人の開発や仕事での開発に活かせばいい!!

本記事の流れ

  • 開発の初期段階のラフ画から、デプロイして少しずつ現在(2019/12/30)の状態までを画像を比較することで追っていきます
  • 以下のような順番でいこうと思います
    • コーディング前のラフ画
    • 最初のデプロイ
    • 1日目の途中経過
    • 1日目の終わり
    • 2日目の途中経過
    • 2日目の終わり
    • 3日目のオプション
  • 年末なのに疲れたくないと思うので、軽くスワイプしてざっーと見るぐらいでいいので読んでみてください
    • 中堅の方達には初期の開発の時の気持ちを思い出していただけると思います
    • 開発初心者に近い人たちにとってはこうやって進めていけば続くのか!という1つの指標みたいな感じにしてもいいかもしれないです

コーディング前のラフ画

  • いきなりコーディングするのは避け、イメージだけでもいいので紙に書き起こしましょう
  • 仕事で開発する場合は、 FigmaSketch などのデザインツールを使うのもいいでしょう
    • 個人的にはFigmaを使っていますが、個人開発のハッカソンレベルのものであれば紙で十分なので、今回は紙です

トップページ
トップページ

質問ページ
質問ページ

結果画面
結果画面

最初のデプロイ

  • デプロイはFirebaseのHostingサービスを使いました
  • domainを取得しなければ基本的に無料で、コマンド一発でデプロイでき、FirestoreというDBも使うことができる最強のサービスです
    • ぜひ使いましょう(僕が学生の時に知っていたら本当によかった)
  • 最初のデプロイはトップページだけでした
    • 全く使い物にならないけど、デプロイって楽しいから何度でもしたくなります
    • そのうち、デプロイしたくて開発している自分に気付きます(☆ここ重要!!)

top

1日目の途中経過

  • 画像を見ればわかりますがほとんど進んでいません
  • Typescriptとか、React-routerとかほとんど経験がなかったので悪戦苦闘しました
  • ここら辺で、自分が作っているものは意味があるのかという自問自答タイムがありました
    • これを打破できたのは細かいcommitと、とりあえずデプロイするみたいなことをやっていたら手が動き始めたからです

top

  • 無限に質問が作れてしまうところからのスタートでした

question

1日目の終わり

  • 結果画面以外の簡単な実装が終了しました
  • ここからは修正があった部分だけ画像をつけます(枚数が増えてしまうので)

  • トップページのフォームの「断捨離る」ボタンを全てのフォームに記入していないと押せないようにしました(validation)

top form

  • 3つの目標に対してそれぞれ質問ページを作りました
    • ラフ画の時点では「はい・いいえ」でしたが、最終的に「0 ~ 10」で選択できるようにしました
      • グラフ作りやすいし、高い低いを Yes or No の2値で選ぶのなんか違うなと思ったからです

question

  • 悲しいことに1日目の終わりの時点では結果画面まで行かなかったです
  • この時点で開発開始から12時間経ち、ベッドへダイブしました
  • これぐらいは1時間で終わらせられるだろ!!というお気持ちの人もいると思いますが、今回の技術はほとんど初挑戦だったのでお許しください
    • 初めての技術はほとんど全部つまづくので温かい目でみていただけるといいですね
    • どんなエンジニアだって最初はこんなもんです。これから開発したい!と思っている初学者のかたも諦めずに頑張ってください!

result

2日目の途中経過

  • 各種質問に対する答えとして「0 ~ 10」をhtml標準の <input type="range"> を使って実装しました
  • 結果画面にグラフで3つの目標の結果画面を表示できるようにしました

question

  • 結果画面にrechartsを使って、
    • 2日目途中でこれだけ?と思われるかもしれませんが、Typescriptを初挑戦していて、型付けの部分でとんでもなく苦労しました
    • rechartsというグラフ描画用のpackageを、別のプロジェクトにjsで作成し、そこなら簡単に呼び出せるのに、なぜかTypescript(ts)で呼び出そうとすると映らず、一生描画できないのではないかと絶望しました
      • 結果的にTypescriptでは型付けのマッピングみたいなことをしたファイルがないと、型がわからず呼び出すことができていなかったです(@types/~ のpackageを追加しました)
    • 得意じゃない、英語の記事を漁ってでも意地でも答えを見つけに向かいました(公式ドキュメントとかは最初に読みましょう)

result

2日目の終わり

  • 2日目の途中の段階でやりたいことはほぼ終わったので、あとは微調整をしていくことができる状態になったので進捗が少し捗った
  • トップのフォーム部分の文字の色が気に食わなかったので修正した
    • 現状が良いのかという議論はあるけど、最初がダサすぎた

top form

  • Material UI を導入し、 Input とか Button あたりの既存のものを置き換えた
    • これだけで雰囲気出てくる
    • CSSフレームワーク大事(というかデザイナーさんは神)

question

  • 結果画面にグラフだけじゃなくて、それぞれの項目の可視化もしてみた
  • 星の大きさは「Dan Syari」的にいえば、あなたにとって大切なことを大きさで表している
    • 大きい星ほど、あなたにとって大切なことである
    • グラフを象限としてみたときに、第一、第二、第四、第三の順に大切なものになる
    • 重要なことがあなたにとって大切なことですよ、ということを伝えたいのだ

result 1

  • はっきり言って、こんなに文字があるとうざいなと、スクショしてみて強烈に感じた
    • 次の開発には生かしたい

result 2

象限

まとめ

  • さぁ、いかがだったでしょうか
  • 少しずつアプリが良くなっていくのが見えたのではないでしょうか
  • ぶっちゃけ、現時点でめっちゃおしゃれで、実用性があって、いろんな人に響くかときかれれば耳が痛くなるでしょう
    • でも、声を大にして言いたい、「最初はこんなもんだよ!」
    • 家族や、友達、知り合いに使ってもらってそれだけで作ってよかったって思えるのも本当に大事だと思います
    • エンジニアとして同じ技術を使い慣れて実装力を上げていくことも大事です
    • が、コンフォートゾーンから抜け出すこともときには大切です
    • 小さくてもダサくても良いから1年のうちに新しい技術に手を出してみること大事
  • だということが「Dan Syari」を使っていただけるとわかります
    • ぜひ試してみてください!!

https://dan-syari.web.app/#/

3日目のオプション

  • OGP設定
    • TwitterとかFacebookとかに貼ったときにでるサマリみたいなあれです
    • ぜひTwitterやFacebookに貼って、友達や知り合いに送ってみてください
  • ちょっとしたデザイン微修正(margin/paddingとか入力欄のwidthとか)
  • Firestoreの導入

top form

slack

  • めちゃくちゃダサいw
    • でも個人的には結構好き

使った技術の解説などは別の記事として作ろうと思います
少しでも面白いなと思った人や、年末の暇つぶしができて嬉しいなという人や、解説記事も読んでみたいなという人は「いいね!」してくださると嬉しいです(この記事にリンク追加したときに通知が飛ぶようになるので是非、「いいね!」しておくといいかもしれません)

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

Electronで「要素の検証」

本文

初投稿です。
Chromeで右クリックすると出てくる「検証(I)」。
押すとデベロッパーツール内でカーソル直下の要素にジャンプします。
Electron内でも使えるとなにかと便利なので、この機能を再現してみます。

openDevTools

Electronで単にデベロッパーツールを開く場合はwebContents.openDevTools([options])を使います。

const { remote } = require('electron')
const mainWindow = remote.getCurrentWindow()
mainWindow.openDevTools()

modeオプションで表示位置の指定ができます。

mode 状態
right 画面右に表示
bottom 画面下に表示
detach 別ウィンドウに切り離して表示
undocked 別ウィンドウに切り離すが画面内に戻すこともできる
mainWindow.openDevTools({ mode: 'undocked' })

※webviewタグによって作られたwebContentsにはmodeの指定ができないようです。(detachのみ)
https://electronjs.org/docs/api/web-contents#contentsopendevtoolsoptions

inspectElement

一方、webContents.inspectElement(x, y)は指定された位置の要素をデベロッパーツール内で開きます。

以下はマウス右クリックで「要素の検証」メニューを表示するサンプルです。
右クリックを使うアプリだと、機能が干渉してしまうので僕は中央ボタンに割り当てています。

  const { remote } = require('electron')
  const { Menu, MenuItem } = remote
  const mainWindow = remote.getCurrentWindow()

  // 2 にするとマウス中央ボタンでメニュー表示
  const triggerButton = 3

  let clickPosition = null

  const contextMenu = new Menu()

  const inspectElement = new MenuItem({
    label: '要素の検証',
    click () {
      mainWindow.inspectElement(...clickPosition)
    }
  })

  contextMenu.append(inspectElement)

  const onclick = ({ x, y, which }) => {
    if (which === triggerButton) {
      event.preventDefault()
      clickPosition = [x, y]
      contextMenu.popup(mainWindow)
    }
  }

  window.addEventListener('auxclick', onclick)

webpackを使っている場合は以下のif文で囲み、プロダクションビルドで除外することをおすすめします。

if (process.env.NODE_ENV !== 'production') {
...
}

https://electronjs.org/docs/api/web-contents#contentsinspectelementx-y

参考

https://electronjs.org/docs/api/web-contents
https://stackoverflow.com/questions/32636750

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

Electronで「要素の検証」を再現

本文

初投稿です。
Chromeで右クリックすると出てくる「検証(I)」。
押すとデベロッパーツール内でカーソル直下の要素にジャンプします。
Electron内でも使えると開発中なにかと便利なので、この機能を再現してみます。

openDevTools

Electronで単にデベロッパーツールを開く場合はwebContents.openDevTools([options])を使います。

const { remote } = require('electron')
const mainWindow = remote.getCurrentWindow()
mainWindow.openDevTools()

modeオプションで表示位置の指定ができます。

mode 状態
right 画面右に表示
bottom 画面下に表示
detach 別ウィンドウに切り離して表示
undocked 別ウィンドウに切り離すが画面内に戻すこともできる
mainWindow.openDevTools({ mode: 'undocked' })

※webviewタグによって作られたwebContentsにはmodeの指定ができないようです。(detachのみ)
https://electronjs.org/docs/api/web-contents#contentsopendevtoolsoptions

inspectElement

一方、webContents.inspectElement(x, y)は指定された位置の要素をデベロッパーツール内で開きます。

以下はマウス右クリックで「要素の検証」メニューを表示するサンプルです。
右クリックを使うアプリだと、機能が干渉してしまうので僕は中央ボタンに割り当てています。

  const { remote } = require('electron')
  const { Menu, MenuItem } = remote
  const mainWindow = remote.getCurrentWindow()

  // 2 にするとマウス中央ボタンでメニュー表示
  const triggerButton = 3

  let clickPosition = null

  const contextMenu = new Menu()

  const inspectElement = new MenuItem({
    label: '要素の検証',
    click () {
      mainWindow.inspectElement(...clickPosition)
    }
  })

  contextMenu.append(inspectElement)

  const onclick = ({ x, y, which }) => {
    if (which === triggerButton) {
      event.preventDefault()
      clickPosition = [x, y]
      contextMenu.popup(mainWindow)
    }
  }

  window.addEventListener('auxclick', onclick)

webpackを使っている場合は以下のif文で囲み、プロダクションビルドで除外することをおすすめします。

if (process.env.NODE_ENV !== 'production') {
...
}

https://electronjs.org/docs/api/web-contents#contentsinspectelementx-y

参考

https://electronjs.org/docs/api/web-contents
https://stackoverflow.com/questions/32636750

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

Electronで「要素の検証」を再現する方法

本文

初投稿です。
Chromeで右クリックすると出てくる「検証(I)」。
押すとデベロッパーツール内でカーソル直下の要素にジャンプします。
Electron内でも使えると開発中なにかと便利なので、この機能を再現してみます。

openDevTools

Electronで単にデベロッパーツールを開く場合はwebContents.openDevTools([options])を使います。

const { remote } = require('electron')
const webContents = remote.getCurrentWindow()
webContents.openDevTools()

modeオプションで表示位置の指定ができます。

mode 状態
right 画面右に表示
bottom 画面下に表示
detach 別ウィンドウに切り離して表示
undocked 別ウィンドウに切り離すが画面内に戻すこともできる
webContents.openDevTools({ mode: 'undocked' })

※webviewタグによって作られたwebContentsにはmodeの指定ができないようです。(detachのみ)
https://electronjs.org/docs/api/web-contents#contentsopendevtoolsoptions

inspectElement

一方、webContents.inspectElement(x, y)は指定された位置の要素をデベロッパーツール内で開きます。

以下はマウス右クリックで「要素の検証」メニューを表示するサンプルです。
右クリックを使うアプリだと、イベントが干渉してしまうので僕は中央ボタンに割り当てています。

  const { remote } = require('electron')
  const { Menu, MenuItem } = remote
  const webContents = remote.getCurrentWindow()

  // 2 にするとマウス中央ボタンでメニュー表示
  const triggerButton = 3

  let clickPosition = null

  const contextMenu = new Menu()

  const inspectElement = new MenuItem({
    label: '要素の検証',
    click () {
      webContents.inspectElement(...clickPosition)
    }
  })

  contextMenu.append(inspectElement)

  const onclick = ({ x, y, which }) => {
    if (which === triggerButton) {
      event.preventDefault()
      clickPosition = [x, y]
      contextMenu.popup(webContents)
    }
  }

  window.addEventListener('auxclick', onclick)

webpackを使っている場合は以下のif文で囲み、プロダクションビルドで除外することをおすすめします。

if (process.env.NODE_ENV !== 'production') {
...
}

https://electronjs.org/docs/api/web-contents#contentsinspectelementx-y

参考

https://electronjs.org/docs/api/web-contents
https://stackoverflow.com/questions/32636750

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

ReactのBDD勉強としてサービス作ってみた

TL;DR

  • フロントエンドBDDの勉強用に、簡単なサービス作った。
  • テスト書くの良い。
  • サービス公開とか色々本筋でないとこに結構引っかかった。
  • Javascript難しい。
  • フロントエンド難しい。

What's this?

最近業務でswiftを触っていたのもあり、勉強の方向性がフロントエンドに偏りつつある。
Reactは以前から学習に取り組み、簡易な実装経験があったが、
そもそも機能を実現することも重要だが、システムとして長期運用していく上で重要になるテストコードについても勉強したいと思ったので、じゃあReactでテスト書きつつシステム一個作って見よう!という試みで開始した。

作ったもの

image.png

サービス:https://timer-d73c3.web.app
GitHub:https://github.com/theMistletoe/RecoSta

起動中の経過時間を計測してるサービス。勉強時間・作業時間を記録することが目的。
エンジニアたるもの20h/weekは勉強するのは最低ラインとのことで、
私はどのくらいできているかな〜というのが気になった、ところから作ってみました。

syougakusei.png

使用したテストライブラリ

https://github.com/testing-library/react-testing-library

react-testing-libraryというライブラリで、Reactに置ける振る舞い駆動テストを実現するためのライブラリを使った。
swiftでもBDDをしていたこともあり、今後の潮流的にもBDD来てそうだなと思ったので使ってみた。
参考で詳しく書いてくださってる記事を載せています。

実際に書いてたテストコードが下記

describe("Main Page",  () => {

        it("get and display studytimes", async () => {
            const spy = jest.spyOn(axios, 'get').mockImplementation(() => {
                return {
                    data: [{date: '20191121', studytime: '2315'}, {date: '20320408', studytime: '444'}]
                }
            });

            const { getByText, getAllByTestId, getByPlaceholderText } = await render(<Main />);

            await waitForElement(() => getAllByTestId("studytime-list"));

            expect(firebase.auth().currentUser.getIdToken).toHaveBeenCalled();
            expect(spy).toHaveBeenCalledWith(`${process.env.REACT_APP_BACKEND_ENDPOINT}/api/v1/studytime`, 
            {headers: { authorization: `Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` }});

            expect(getByText("Total: You've studieds 2759 seconds!")).toBeInTheDocument();
            expect(getByPlaceholderText("Input Your Email Address")).toBeInTheDocument();

            expect(getByText("Now, You've studied")).toBeInTheDocument();
            expect(getByText("Date")).toBeInTheDocument();
            expect(getByText("Studied Times(s)")).toBeInTheDocument();
            expect(getByText("20191121")).toBeInTheDocument();
            expect(getByText("2315")).toBeInTheDocument();
            expect(getByText("20320408")).toBeInTheDocument();
            expect(getByText("444")).toBeInTheDocument();
        });
}

BDDっぽいところでいうと、
expect(getByText("20191121")).toBeInTheDocument();
みたいにHTMLの画面上に想定通りの文字や入力欄が存在するかどうかをテストコードで表現できる。
こうすることで、ユーザーからみた挙動に近い形で、テストコードをSpecとして表現できる。

システム構成

構成自体はJAMStack?のはず?です。

システム構成図 (1).png

Firebaseを中心に活用しつつ、バックエンドだけHeroku使ってる感じです。
Firebase HostingにReactのフロントエンドをデプロイしています。
バックエンドもNode.js(express)でさっくり書いています。

データストアはFireStoreというFirebaseのNoSQLを使用しています。エンティティ設計がちとめんどくさいですが、簡単なサービスを作るときには簡単にできて良い感じですね。

認証にもFirebase Authenticationを使用していて、メールアドレスによる認証を行なっています。
この辺の認証周りとかすごいめんどくさかった、、自分が理解できていないこともあるが、、、

そもそもFirebaseのmBaasの考え方的にバックエンドを挟む構成がいまいち向いていない感じがありますね。初めはバックエンド書かずにやろうとしたんですが、axiosのHTTPリクエストのテストがしたかったのと、よくわからなかったので逃げてしまった、、、

まとめ

テスト・BDDの勉強のためにサービス作りながら実際に適用できそうか勉強しながらやってみて、
BDD・TDDを実践しながら作ると、個人開発であっても実装に迷うことが少なくなり、方向性を見失うことはなかったように思う。
が、ここで早くなったと書かなかったのは、テストの書き方とか、Jestわからんとか、Mock効かねぇとか、テスト周りで実装に詰まることが格段に増え、すっごい時間がかかってしまった。

そもそもJavascriptへの理解が全然追いついていないとひしひしと感じた。バックエンドで使ってるような言語のスタンスで取り組むと、全然期待する挙動にならずオワタ...になる。

一旦ベースができたので、 実はテスト書けてないところとか、設計オワコンなところとか、 改善点を直しながら理解を深める活動ができればいいな、と思っている。

参考

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

Node.js の Async Hooks API の動作を検証しました

必要に迫られて、Node.js の Async Hooks API について調べたので、その仕組を実例を用いて説明します。

Async Hooks とは?

Node.js の Stability: 1 - Experimental (2019/12/30 現在) な機能です。
主に 非同期呼出を追跡する のに使われています。例えば以下の様な NPM Module が Async Hooks を使っています。

  • longjohn → 非同期呼出で途切れる Stack trace を繋げて表示する
  • trace → 非同期呼出で途切れる Stack trace を繋げて表示する
  • express-http-context → リクエスト毎に異なるコンテキストに値を保存/取得する express middleware

対象読者

事前に Node.jsでのイベントループの仕組みとタイマーについて について知っておくことをオススメします。

動作環境

本記事は以下の環境で試しています。

  • Mac OS 10.13.6
  • Node.js v12.13.0

私は nodebrew を使っています。

nodebrew binary-install v12.13.0
nodebrew use v12.13.0

コード

記事で書くコードそのものが↓です。

Async Hooks の概要

公式の Node.js >> Async Hooks >> Overview の通りですが、実際に動かして見ないとドキュメントだけ読んでも分かりづらいです。まずはざっくり概要です。

Async Hooks 自体は、以下の様に async_hooks.createHook() するだけで使えます。

const async_hooks = require('async_hooks');

// 現在 (↓が実行されてる瞬間) の Async ID を取得できます。
const eid = async_hooks.executionAsyncId();

// Async Hook を生成します。
// ここで渡した4つの callback functions が、非同期呼び出しの際に呼ばれる様になります。
const asyncHook = async_hooks.createHook({
  init,
  before,
  after,
  destroy,
});

// 有効にしないと callback functions が呼ばれません。
asyncHook.enable();

前述の async_hooks.createHook() に渡した4つの callback functions は自由に実装できます。
今回は、確認の為に、呼出の際の引数値を Array に保存しておく様にしました。

// [init] setTimeout() 等の非同期処理 (callback function) の登録をした時に呼ばれます。
function init(asyncId, type, triggerAsyncId, resource) {
  // Promise による callback task 登録の場合、Promise オブジェクト自身が来るので、
  // ちゃんと GC されるように参照保持しないようにします。
  arguments[3] = resource.constructor.name === 'PromiseWrap' ? resource.toString() : resource;

  historyInit.push(arguments);
}

// [before] 登録した callback function が実行される「直前」に呼ばれます。
function before(asyncId) {
  historyBefore.push(asyncId);
}

// [after] 登録した callback function が実行された「後」に呼ばれます。
function after(asyncId) {
  historyAfter.push(asyncId);
}

// [destroy] 非同期リソースが破棄 (≒登録した非同期処理の完了) された時に呼ばれます。
function destroy(asyncId) {
  historyDestroy.push(asyncId);
}

// Async Hooks の動作確認の為に、init, before, after, destroy 呼出の際の引数値をここ↓に保存しておきます。
const historyInit = [];
const historyBefore = [];
const historyAfter = [];
const historyDestroy = [];

この状態で setTimeout() 等を実行すると、 init, before, after, destroy callback functions が順に呼ばれるようになります。

init, before, after, destroy の定義

時系列

API setInterval での例です。

setInterval.png

init

非同期 API (setTimeout, Promise 等) を実行した瞬間に呼ばれます。

別の言い方をすると、非同期処理 (callback functions) が event queue に登録された時です。

  • 1度だけ 必ず 呼ばれます
引数名 説明
asyncId Number 分岐元 (親) の Async ID
type String Timeout, PROMISE 等の識別子が来ます。resource の名称です。
triggerAsyncId Number 分岐元 (親) の Async ID
resource Object 実行した非同期処理の情報。Promise の場合、promise object 自身が来るので、参照保持で GC を阻害しないように注意。

before

登録した callback function が実行される 直前 に呼ばれます。

  • 一度も呼ばれない事があります 例えば net.createServer で Socket listen していても、接続がなければ callback 実行されません
  • setInterval 等、一度の callback 登録で before複数回 呼ばれる事があります
引数名 説明
asyncId Number 分岐元 (親) の Async ID

after

登録した callback function が実行された に呼ばれます。

  • before と同様に、 一度も呼ばれない もしくは 複数回 呼ばれる事があります
  • callback で例外が発生し catch されなかった場合、 uncaughtException event もしくは handler 実行の後に、after が呼ばれます
引数名 説明
asyncId Number 分岐元 (親) の Async ID

destroy

非同期リソース resource が 破棄 (≒登録した非同期処理の完了) された時に 一度だけ 呼ばれます。

具体的にいつ呼ばれるかはまちまちで、例えば Promise の場合は promise object が GC により破棄された際に呼ばれます

引数名 説明
asyncId Number 分岐元 (親) の Async ID

Examples

ドキュメントを読んだだけでは、init, before, after, destroy callback functions がそれぞれ、どのタイミングで、何回呼ばれるのか、よく分かりません。

実際に実行して試してみます。

下準備

前述のコードをファイル名 register-hook.js とし、以下の Debug Print 関数を module.exports します。

わざと setTimeout で表示処理を非同期実行してます。

register-hook.js
const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({ init, before, after, destroy });
asyncHook.enable();

// ...
// 省略...
// ...

/**
 * async_hooks で取得した非同期呼び出しの履歴を表示します.
 */
module.exports = function () {
  // 表示処理が呼出元とは異なる event task として実行される様、setTimeout する. (event queue に入れておく)
  setTimeout(function printHistory() {
    const [init, before, after, destroy]
      = [[...historyInit], [...historyBefore], [...historyAfter], [...historyDestroy]];

    console.log('FirstExecutionAsyncId: ', eid, '\n');
    console.log('async_hook calls: init: ', init, '\n');
    console.log('async_hook calls: before: ', before, '\n');
    console.log('async_hook calls: after: ', after, '\n');
    console.log('async_hook calls: destroy: ', destroy, '\n');
  }, 10);
}

(1) setTimeout で Async Hooks

example-setTimeout.png

まずは Async Hooks を有効にした状態で setTimeout を呼ぶとどうなるか見てみます。

index_setTimeout.js
const printHistory = require('./register-hook');

function _01_SetTimeoutCallbackFunction() {
  printHistory(); // ← setTimeout 経由で console.log する.
}

setTimeout(_01_SetTimeoutCallbackFunction);

以下の様に printHistory() で出力されます。

FirstExecutionAsyncId:  1 

async_hook calls: init:  [
  [Arguments] {
    '0': 2,
    '1': 'Timeout',
    '2': 1,
    '3': Timeout {
      _idleTimeout: 1,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 44,
      _onTimeout: [Function: _01_SetTimeoutCallbackFunction],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: true,
      [Symbol(refed)]: null,
      [Symbol(asyncId)]: 2,
      [Symbol(triggerId)]: 1
    }
  },
  [Arguments] {
    '0': 3,
    '1': 'Timeout',
    '2': 2,
    '3': Timeout {
      _idleTimeout: 10,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 46,
      _onTimeout: [Function: printHistory],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: false,
      [Symbol(refed)]: true,
      [Symbol(asyncId)]: 3,
      [Symbol(triggerId)]: 2
    }
  }
] 

async_hook calls: before:  [ 2, 3 ] 

async_hook calls: after:  [ 2 ] 

async_hook calls: destroy:  [ 2 ] 

エントリファイルの index_setTimeout.js が実行された直後の Async ID は 1 です

init は2回呼ばれています。

1回目は setTimeout(_01_SetTimeoutCallbackFunction) (id=2) で、 index_setTimeout.js ファイル自身 (id=1) からトリガーされた非同期タスクである事が分かります。

2回目は setTimeout(printHistory) (id=3) で、先程の setTimeout(_01_SetTimeoutCallbackFunction) (id=2) からトリガーされた非同期タスクである事が分かります。

before は2回呼ばれています。

function _01_SetTimeoutCallbackFunction (id=2) と printHistory (id=3) が呼出されたからです

after は1回呼ばれています。

function _01_SetTimeoutCallbackFunction (id=2) の実行は完了したが、 printHistory (id=3) はまだ途中だからです

destroy は1回呼ばれています。

function _01_SetTimeoutCallbackFunction (id=2) の破棄は完了したが、 printHistory (id=3) はまだ実行途中で破棄されてないからです

(2) setInterval で Async Hooks

example-setInterval.png

次に Async Hooks を有効にした状態で setInterval を呼ぶとどうなるか見てみます。

index_setInterval.js
const printHistory = require('./register-hook');

let count = 0;

function _01_SetIntervalCallbackFunction() {
  if (++count > 10) {
    clearInterval(intervalID);
    printHistory(); // ← setTimeout 経由で console.log する.
  }
}

const intervalID = setInterval(_01_SetIntervalCallbackFunction, 10);

以下の様に printHistory() で出力されます。

FirstExecutionAsyncId:  1 

async_hook calls: init:  [
  [Arguments] {
    '0': 2,
    '1': 'Timeout',
    '2': 1,
    '3': Timeout {
      _idleTimeout: -1,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 157,
      _onTimeout: null,
      _timerArgs: undefined,
      _repeat: 10,
      _destroyed: true,
      [Symbol(refed)]: null,
      [Symbol(asyncId)]: 2,
      [Symbol(triggerId)]: 1
    }
  },
  [Arguments] {
    '0': 3,
    '1': 'Timeout',
    '2': 2,
    '3': Timeout {
      _idleTimeout: 10,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 171,
      _onTimeout: [Function: printHistory],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: false,
      [Symbol(refed)]: true,
      [Symbol(asyncId)]: 3,
      [Symbol(triggerId)]: 2
    }
  }
] 

async_hook calls: before:  [
  2, 2, 2, 2, 2,
  2, 2, 2, 2, 2,
  2, 3
] 

async_hook calls: after:  [
  2, 2, 2, 2, 2,
  2, 2, 2, 2, 2,
  2
] 

async_hook calls: destroy:  [ 2 ]

init は2回呼ばれています。

1回目は setInterval(_01_SetIntervalCallbackFunction, 10) (id=2) で、 index_setInterval.js ファイル自身 (id=1) からトリガーされた非同期タスクである事が分かります。

2回目は setTimeout(printHistory) (id=3) で、先程の setTimeout(_01_SetIntervalCallbackFunction) (id=2) からトリガーされた非同期タスクである事が分かります。

before は11回呼ばれています。

このコードでは、1度の setInterval(_01_SetIntervalCallbackFunction, 10) で callback function _01_SetIntervalCallbackFunction (id=2) は 10 ms おきに実行され、clearInterval() されるまで 計10回 呼ばれています。

また、 printHistory (id=3) も1度呼出され、カウントされます。

after は10回呼ばれています。

before で述べた通り、_01_SetIntervalCallbackFunction (id=2) は 計10回 実行されました。

printHistory (id=3) の処理はまだ途中の為、カウントされません。

destroy は1回呼ばれています。

clearInterval の実行により callback function _01_SetTimeoutCallbackFunction (id=2) は破棄されます。

printHistory (id=3) はまだ実行途中で破棄されてないので、カウントされません。

(3) Promise で Async Hooks

example-promise.png

最後に Async Hooks を有効にした状態で new Promise(callback) を呼ぶとどうなるか見てみます。

index_promise.js
const printHistory = require('./register-hook');

function _01_PromiseCallbackFunction (resolve, _) {
  resolve();
}

function _02_PromiseThenCallbackFunction (_) {
  // GC で Promise オブジェクトを破棄しないと "async_hooks.destroy" callback は呼ばれない.
  setTimeout(global.gc);

  printHistory(); // ← setTimeout 経由で console.log する.
}

new Promise(_01_PromiseCallbackFunction)
  .then(_02_PromiseThenCallbackFunction);

以下の様に printHistory() で出力されます。

FirstExecutionAsyncId:  1 

async_hook calls: init:  [
  [Arguments] {
    '0': 2,
    '1': 'PROMISE',
    '2': 1,
    '3': '[object PromiseWrap]'
  },
  [Arguments] {
    '0': 3,
    '1': 'PROMISE',
    '2': 2,
    '3': '[object PromiseWrap]'
  },
  [Arguments] {
    '0': 4,
    '1': 'Timeout',
    '2': 3,
    '3': Timeout {
      _idleTimeout: 1,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 49,
      _onTimeout: [Function: gc],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: true,
      [Symbol(refed)]: null,
      [Symbol(asyncId)]: 4,
      [Symbol(triggerId)]: 3
    }
  },
  [Arguments] {
    '0': 5,
    '1': 'Timeout',
    '2': 3,
    '3': Timeout {
      _idleTimeout: 10,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 49,
      _onTimeout: [Function: printHistory],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: false,
      [Symbol(refed)]: true,
      [Symbol(asyncId)]: 5,
      [Symbol(triggerId)]: 3
    }
  }
] 

async_hook calls: before:  [ 3, 4, 5 ] 

async_hook calls: after:  [ 3, 4 ] 

async_hook calls: destroy:  [ 2, 3, 4 ]

ちょっとごちゃごちゃしているのは、new Promise(callback) に加え、
Promise.then() の呼出と、setTimeout(global.gc) の呼出があるからです。

init は4回呼ばれています。

1回目は new Promise(_01_PromiseCallbackFunction) (id=2) で、 index_promise.js ファイル自身 (id=1) からトリガーされた非同期タスクである事が分かります。

2回目は promise.then(_02_PromiseThenCallbackFunction) (id=3) で、 new Promise(_01_PromiseCallbackFunction) (id=2) からトリガーされた非同期タスクである事が分かります。

3回目は意図的にコード setTimeout(global.gc) を記述した為に、記録されています。

destroy で後述しますが、 Promise の場合は GC で promise object 破棄されるまで destroy が呼出されません

4回目は setTimeout(printHistory) (id=5) で、先程の promise.then(_02_PromiseThenCallbackFunction) (id=3) からトリガーされた非同期タスクである事が分かります。

before は3回呼ばれています。

正直意図しない動きをしています。

  • new Promise(_01_PromiseCallbackFunction) (id=2) では before呼出されない
  • promise.then(_02_PromiseThenCallbackFunction) (id=3) では before呼出される

以下の通り、公式のドキュメントにもそれとなく書いてありますが、どうしてこういう動作になるのか理解できていません。

after は2回呼ばれています。

before と同様です。

printHistory (id=5) の処理はまだ途中の為、カウントされません。

destroy は3回呼ばれています。

Promise の場合、promise object が 破棄 (=GC) されるまで destroy は呼出されません。

本コードでは、意図的に global.gc() を実行しました。

printHistory (id=5) はまだ実行途中で破棄されてないので、カウントされません。

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

AWS+NodeJSでサーバレスな環境構築①

はじめに

サーバレスは完全に未経験ですが、勉強がてら備忘録として残しておこうと思います。
表現等が正しく無い場合はご指摘いただければ、幸いです。

サーバレスってなぁに?

簡単に言ってしまうと、ユーザーがサーバー領域を意識せず、直接利用出来るサービスを活用した構成のことです。
「Lambda」は設定されているプログラムを起動させる実行環境となります。起動条件が整った際に、プログラムをLambda環境に呼び出し、実行されます。この為、Lambdaでは、実行した時間とその回数のみの課金となります。

向いているサービス
・待機時間の長いシステム
・CPUの負荷が時間帯によって差のあるシステム

不向きなサービス
・常にシステムの動いている必要のあるサービス
・高負荷な状態が長時間続くシステム

もう少し詳しいことを知りたい場合はこちらのサイトをご覧ください。
サーバーレス アーキテクチャ

Lambda関数の作成

・AWS Lambdaページ>関数の作成>一から作成
・関数名を入力、ランタイムの選択(必要であれば)
・その他はデフォルト値のままで作成ボタン

API Gatewayのトリガーを追加と設定

・「トリガーを追加」ボタンを押し、API Gatewayを選択
・APIを「新規のAPI」を選択
・その他はデフォルト値のままで追加ボタン

ソース

index.js
'use strict'

let fs = require('fs')
let path = require('path')

exports.handler = (event, context, callback) => {


  let filePath = path.join(__dirname, 'page.html')
  let html = fs.readFileSync(filePath).toString()

  sendHtmlResponse(context, 200, html)

}

function sendHtmlResponse(context, statusCode, html) {
  let response = {
    'statusCode': statusCode,
    'headers': {
      'Content-Type': 'text/html'
    },
    'body': html
  }
  context.succeed(response)
}
page.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>サーバレス</title>
</head>

<body>
    <h1>やっちゃおう!サーバレスで</h1>
</body>
</html>

ページにアクセス

API Gatewayの設置したトリガーをクリックし、表示されてるURLをクリックすれば、アクセスできるはずです。
スクリーンショット 2019-12-30 17.28.03.png
遷移先のページでこのように表示されれば、成功です。
スクリーンショット 2019-12-30 18.21.51.png

終わりに

次はLambdaとdynamodbやS3と組み合わせて投稿しようかと思います。

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

なっがいSQLをNode.jsで生成してみた

背景

  • 似たような処理の繰り返しなので共通化できそう
  • でも、SQLの知識がそこまでない
  • Gitでコード管理しているが、なっがいSQLをプッシュすると、全コードにおけるSQLの割合が増えてなんか気分が悪い

なっがいSQLサンプル

SELECT箇所・LEFT JOIN箇所で多数の重複があり、見やすくするため省略しています

-- CREATE TABLE sample_table
SELECT
  pb.id, p.name, p.team,
  -- bat1
  IFNULL(fst.slug_ave, 0) AS rate1, 
  IFNULL(fst.pa, 0) AS pa1,
  IFNULL(fst.ab, 0) AS ab1,
  IFNULL(fst.tb, 0) AS cnt1,
  -- bat2
  -- ︙
  -- bat3
  -- ︙
  -- bat4
  -- ︙
  -- bat5
  -- ︙
  -- bat6
  -- ︙
  -- bat7
  IFNULL(sev.slug_ave, 0) AS rate7,
  IFNULL(sev.pa, 0) AS pa7,
  IFNULL(sev.ab, 0) AS ab7,
  IFNULL(sev.tb, 0) AS cnt7,
  -- 各項目合計
  CASE WHEN(IFNULL(fst.ab, 0) + IFNULL(scd.ab, 0) + IFNULL(thr.ab, 0) + IFNULL(fur.ab, 0) + IFNULL(fif.ab, 0) + IFNULL(six.ab, 0) + IFNULL(sev.ab, 0)) > 0 THEN ROUND((IFNULL(fst.tb, 0) + IFNULL(scd.tb, 0) + IFNULL(thr.tb, 0) + IFNULL(fur.tb, 0) + IFNULL(fif.tb, 0) + IFNULL(six.tb, 0) + IFNULL(sev.tb, 0))/(IFNULL(fst.ab, 0) + IFNULL(scd.ab, 0) + IFNULL(thr.ab, 0) + IFNULL(fur.ab, 0) + IFNULL(fif.ab, 0) + IFNULL(six.ab, 0) + IFNULL(sev.ab, 0)), 5) ELSE NULL END AS rate,
  IFNULL(fst.ab, 0) + IFNULL(scd.ab, 0) + IFNULL(thr.ab, 0) + IFNULL(fur.ab, 0) + IFNULL(fif.ab, 0) + IFNULL(six.ab, 0) + IFNULL(sev.ab, 0) AS ab,
  IFNULL(fst.pa, 0) + IFNULL(scd.pa, 0) + IFNULL(thr.pa, 0) + IFNULL(fur.pa, 0) + IFNULL(fif.pa, 0) + IFNULL(six.pa, 0) + IFNULL(sev.pa, 0) AS pa,
  IFNULL(fst.tb, 0) + IFNULL(scd.tb, 0) + IFNULL(thr.tb, 0) + IFNULL(fur.tb, 0) + IFNULL(fif.tb, 0) + IFNULL(six.tb, 0) + IFNULL(sev.tb, 0) AS cnt,
  'e' AS eol
FROM baseball._player_batter pb
  LEFT JOIN player p ON pb.id = p.id
  -- bat1
  LEFT JOIN (
    SELECT 
          h.batter,
      COUNT(h.batter OR NULL) AS pa,
      COUNT(eb.name IS NULL OR NULL) AS ab,
      COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4 AS tb,
      CASE WHEN COUNT(eb.name IS NULL OR NULL) > 0 THEN ROUND((COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4) / COUNT(eb.name IS NULL OR NULL), 5) ELSE null END AS slug_ave
    FROM
        baseball._bat_all_info h
    LEFT JOIN exclude_batting_info eb ON h.`1_result` = eb.name
    LEFT JOIN hit_id_info hi ON  h.`1_rst_id` = hi.rst_id
    WHERE h.`1_result` IS NOT NULL
    GROUP BY batter
  ) AS fst ON fst.batter = pb.id
  -- bat2
  -- ︙
  -- bat3
  -- ︙
  -- bat4
  -- ︙
  -- bat5
  -- ︙
  -- bat6
  -- ︙
  -- bat7
  LEFT JOIN (
    SELECT 
          h.batter,
      COUNT(h.batter OR NULL) AS pa,
      COUNT(eb.name IS NULL OR NULL) AS ab,
      COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4 AS tb,
      CASE WHEN COUNT(eb.name IS NULL OR NULL) > 0 THEN ROUND((COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4) / COUNT(eb.name IS NULL OR NULL), 5) ELSE null END AS slug_ave
    FROM
        baseball._bat_all_info h
    LEFT JOIN exclude_batting_info eb ON h.`7_result` = eb.name
    LEFT JOIN hit_id_info hi ON  h.`7_rst_id` = hi.rst_id
    WHERE h.`7_result` IS NOT NULL
    GROUP BY batter
  ) AS sev ON sev.batter = pb.id
;

共通化できそうなポイント

  • SELECT箇所
    • rate, pa, ab, cntの4カラムについて、末尾に1~7をそれぞれ付与しただけ
    • 各項目の合計算出カラム
      • カラム名を連結するだけなのでコードで簡単に実現できそう
  • baseball._player_batterLEFT JOINを7回繰り返している箇所
    • その中のSQLについても、ほとんど同じ
    • 違うのは以下の2点
      • _bat_all_infoLEFT JOINする際の結合条件やWHERE句で指定するカラムが1_result7_resultであること
      • LEFT JOINした後のAlias (fst~sev)

共通化コード

requireしている他のコードについては省略させていただきます。

  • execute: fsモジュールでsql形式のファイルを出力する
  • getFileName: ファイルのフルパスからファイル名のみを抽出する
  • cols: カラム名を+で連結した結果の末尾3文字を削除する
// average_slugging.js

"use strict";

const { execute, getFilename, cols } = require("./util/func");
const { BATS_COL } = require("../constants");

let sql = `-- CREATE TABLE ${getFilename(__filename)}
`;

// -------------------- [select part] --------------------
// player_info
sql += `SELECT
  pb.id, p.name, p.team,
  `;

let abCols = "";
let paCols = "";
let tbCols = "";

// any info(rate, pa, ab, cnt) per inning
Object.keys(BATS_COL).map(bat => {
  const batName = BATS_COL[bat];
  sql += `-- bat${bat}`;
  sql += `
  IFNULL(${batName}.slug_ave, 0) AS rate${bat},
  IFNULL(${batName}.pa, 0) AS pa${bat},
  IFNULL(${batName}.ab, 0) AS ab${bat},
  IFNULL(${batName}.tb, 0) AS cnt${bat},
  `;

  abCols += `IFNULL(${batName}.ab, 0) + `;
  paCols += `IFNULL(${batName}.pa, 0) + `;
  tbCols += `IFNULL(${batName}.tb, 0) + `;
});

// about `total`
sql += `-- 各項目合計`;
sql += `
  CASE WHEN(${cols(abCols)}) > 0 THEN ROUND((${cols(tbCols)})/(${cols(abCols)}), 5) ELSE NULL END AS rate,
  ${cols(abCols)} AS ab,
  ${cols(paCols)} AS pa,
  ${cols(tbCols)} AS cnt,
  `;
// -------------------- /[select part] --------------------

sql += `'e' AS eol
FROM baseball._player_batter pb
  LEFT JOIN player p ON pb.id = p.id`;

// -------------------- [left join part] --------------------
// left join part per inning
Object.keys(BATS_COL).map(bat => {
  const batName = BATS_COL[bat];
  sql += `-- bat${bat}`;
  sql += `
  LEFT JOIN (
    SELECT 
          h.batter,
      COUNT(h.batter OR NULL) AS pa,
      COUNT(eb.name IS NULL OR NULL) AS ab,
      COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4 AS tb,
      CASE WHEN COUNT(eb.name IS NULL OR NULL) > 0 THEN ROUND((COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4) / COUNT(eb.name IS NULL OR NULL), 5) ELSE null END AS slug_ave
    FROM
        baseball._bat_all_info h
    LEFT JOIN exclude_batting_info eb ON h.\`${bat}_result\` = eb.name
    LEFT JOIN hit_id_info hi ON  h.\`${bat}_rst_id\` = hi.rst_id
    WHERE h.\`${bat}_result\` IS NOT NULL
    GROUP BY batter
  ) AS ${batName} ON ${batName}.batter = pb.id
  `;
});
// -------------------- /[left join part] --------------------

// generate
execute(`${getFilename(__filename)}`, sql);

実行方法

$ node average_slugging.js

SQLを生成する利点

  • とにかくSQLの修正が容易になる
    • 例えば現在の5カラム(batter, pa, ab, tb, slug_ave)以外に出力したいカラムがある場合、1箇所を修正するだけでLEFT JOIN7箇所全てを反映できる
    • 7箇所全てを少しずつ修正するのは馬鹿らしい
  • 新たなSQLを生成するのにも役に立つ
    • 今回はプロ野球選手の各打席(第1打席〜第7打席)の長打率を求めるSQLでしたが、出塁率や打率などのSQLを作成する際はすぐに作成することができる
  • 生成したSQLは.gitignoreに追加することでGitでの管理が不要
    • 生成するこのコードのみ管理することで、コードにおけるSQLの内訳が減る

まとめ

よくいろんなエンジニアもおっしゃっている楽をするために苦労するということを実践してみました
今後も何かめんどくさいな、と思ったことに対して、楽に何かできることがあれば投稿します

今回Node.jsで書いたこの内容をSQLでできる方法をご存知でしたら、ぜひご教授いただきたいです
最後まで読んでくださりありがとうございました

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