Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8820 articles
Browse latest View live

【Node.js】Firebaseでエミュレーターを使ってjestのCIをGithub Actionsで

$
0
0

もうエミュレーターでテストすればいいんじゃないか説。

package.json
"scripts":{"jest":"jest","test":"firebase emulators:exec --only functions,firestore \"npm run jest --exit\"",}

別のFirebaseプロジェクトを用意する必要もないし、sinonでFirebaseAdminのモックとかも作る必要ないから楽。

Github Actions

jobs:
  ci:
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [ubuntu-latest]
        node: [12]

    steps:
      - name: Checkout 🛎
        uses: actions/checkout@master

      - name: Setup node env 🏗
        uses: actions/setup-node@v2.1.2
        with:
          node-version: ${{ matrix.node }}

      - name: Cache node_modules 📦
        uses: actions/cache@v2
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install dependencies 👨🏻‍💻
        run: |
          npm install -g firebase-tools
          npm ci

      - name: Firebase runtime config 🔥
        uses: w9jds/firebase-action@master
        with:
          args: functions:config:get > .runtimeconfig.json
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
          PROJECT_ID: "default"

      - name: Run linter 👀
        run: |
          npm run lint
          npm run test

functions.config()を使ってる場合は、firebase-actionsを使って.runtimeconfig.jsonに吐き出す。
また、エミュレーターの起動はfirebase-actionsを使わないのでnpm install -g firebase-toolsを忘れずに。


【コピペOK】bashでnodeとyarnのインストールをする方法

$
0
0

こんにちは、くりぱんです。

この記事で実現できること

  • homebrewのインストール
  • nodebrewのインストール
  • Node.jsのインストール
  • yarnのインストール

説明

フロントエンドの環境を構築している際にnode.jsとyarnが必要になったので、nodebrewを使って、node.jsとyarnをインストールしていきます。
※なお、今回はbashで実装していきます。

開発環境

  • macOS Catalina
  • bash

実装の流れ

  • Homebrewのインストール
  • Nodebrewのインストール
  • nodebrewのPATHを通す
  • Node.jsのインストール
  • yarnのインストール

実装

Homebrewのインストール

Homebrewはパッケージ管理システムの一つで、様々なソフトウェアの導入を簡単にしてくれるツールです。
今回はこちらを使用していくので、下記のコマンドを実行して、インストールしてください。

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

公式サイト:https://brew.sh/index_ja

nodebrewのインストール

NodebrewはNode.jsのバージョンを管理するバージョン管理ツールです。今回はこのnodebrewを使用して、Node.jsとyarnを一括管理できるようにしていきます。
下記のコマンドを実行して、先ほどインストールしたHomebrewを使用してnodebrewをインストールしてください。

$ brew install nodebrew

これでnodebrewのインストールは終わりです。

nodebrewのPATHを通す

Nodebrewのコマンドを利用するためにnodebrewにPATHを通していきます。
下記のコマンドを実行してください。

$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile

追加したパスを適用していきます。

$ source ~/.bash_profile

これでNode.jsとyarnをインストールする準備が完了しました。

Node.jsのインストール

それでは、nodebrewを使用して、Node.jsをインストールしていきます。

$ nodebrew install-binary latest

インストールが完了したら、インストールされたNode.jsのバージョンを確認して、そのバージョンを使う設定にしていきます。

$ nodebrew list

v15.11.0などのバージョンが表示されていればOKです。このバージョンは人によって違うので、数字が違くても気にしなくて大丈夫です。

バージョンを確認したら、下記コマンドのバージョンを自分がインストールしたバージョンにしてコマンドを実行してください。

$ nodebrew use v15.11.0

下記コマンドでNode.jsのバージョンを確認してください。

$ node -v
v15.11.0

yarnのインストール

最後にyarnのインストールをして終わりです。

$ npm install -g yarn

バージョン確認もしておきましょう

$ yarn -v
1.22.10

最後に

以上でbashを使用したNode.jsとyarnのインストールは終了です。
各々フロントの開発を楽しんじゃってください!

少しでも役に立った!という時は、LGTMをポチッと、、、笑
1つでもLGTMが付くとその日がハッピーになるんです!
役に立たなかった時は、怒らないでコメント頂けると幸いです笑

Twitterもやってます!
プログラミングや金融知識についてやエンジニアの現実についてつぶやいています!
よかったら見てみてくださいね!
https://twitter.com/sakuslife

docker+laravel +nginx+Mysql環境でnpmコマンド使えるように設定する

$
0
0

前提

この記事をもとにdocker+laravel +nginx+Mysqlの環境を構築。

ディレクトリ構造

├── README.md (この名前にするとGitHubで見た時にHTMLに変換して表示してくれる)
├── infra (*1)
│   ├── mysql (*1)
│   │   ├── Dockerfile
│   │   └── my.cnf (*1)
│   ├── nginx (*1)
│   │   └── default.conf (*1)
│   └── php (*1)
│       ├── Dockerfile (この名前にするとファイル名の指定を省略できる)
│       └── php.ini (*1)
├── docker-compose.yml (この名前にするとファイル名の指定を省略できる)
└── backend (*1)
    └── Laravelをインストールするディレクトリ

コンテナの構造

appコンテナ

 このコンテナで、phpやlaravelをインストールしてる

webコンテナ

 このコンテナで、nginxをインストールしてる

dbコンテナ

このコンテナで、MYSQLをインストールしてる

node npmインストール方法

php/Dockerfileを編集

FROM php:7.4-fpm-buster
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

ENV COMPOSER_ALLOW_SUPERUSER=1 \
  COMPOSER_HOME=/composer

COPY --from=composer:1.10 /usr/bin/composer /usr/bin/composer

RUN apt-get update && \
  apt-get -y install git unzip libzip-dev libicu-dev libonig-dev && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  docker-php-ext-install intl pdo_mysql zip bcmath
// 以下を追加{{ここから}}
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN apt-get install -y nodejs
// {{ここまで}}
COPY ./php.ini /usr/local/etc/php/php.ini

WORKDIR /work

Dockerfileを変更したら以下のコマンドでビルドを行う

$docker-composer app build

以下のコマンドでコンテナを起動し、appコンテナ内に入る

$ docker-composer up -d
$ docker-composer exec bash app

appコンテナ内でnode -vコマンドなどを実行して、Node.jsやyarnがインストールされていることを確認できれば、npmコマンドの使用が可能になります!!

参考記事

Docker LaravelのコンテナでNode.jsやyarnを導入する

veritas-s3-transformを使ってみた

$
0
0

S3に画像などをアップロードするにはmulter-s3を使っているのですが、「サムネイルも欲しい、そしてcontent-typeも設定して欲しい」というリクエストに対応した話です。

他のモジュールも試してみたのですが、content-typeの指定をできるものが見当たらず「みなさん、S3に画像アップロードするときはapplication/octet-streamでやるのが標準なのだろうか?」と思っていたところ、veritas-s3-transformを見つけました。

TypeScriptで書かれているということで、型定義ファイルもあると期待したのですが、index.d.ts

export {};

というスッキリした世界だったので、自力で型定義ファイルを雑に用意しています(@types/multer-s3をベースtransformやshouldTransformを追加すれば大丈夫でした)。

変換ルールの記述

基本的にはmulter-s3と同じ感じで使えますが、shouldTransformtransformsという、アップロード時の変換定義が入ります。

shouldTransform:function(req:Express.Request,file:Express.Multer.File,cb:(error:null,key:boolean)=>void){cb(null,file.fieldname=='image2')},transforms:[{id:'original',key:function(req:Express.Request,file:Express.Multer.File,cb:(error:null,key?:string)=>void){cb(null,Date.now().toString()+file.originalname)},transform:function(req:Express.Request,file:Express.Multer.File,cb:(error:null,key?:sharp.Sharp)=>void){cb(null,sharp())}},{id:'thumbnail',key:function(req:Express.Request,file:Express.Multer.File,cb:(error:null,key?:string)=>void){cb(null,Date.now().toString()+'thumb'+file.originalname)},transform:function(req:Express.Request,file:Express.Multer.File,cb:(error:null,key?:sharp.Sharp)=>void){cb(null,sharp().resize(100,100))}}]

上記の場合は、fieldnameがimage2の場合のみ変換を行う。オリジナルとサムネイルをアップロードするが、オリジナルは変換なしで、サムネイルはリサイズしてアップロード、という意味になります。

content-typeの指定

multer-s3と同じです

contentType:function(req:Express.Request,file:Express.Multer.File,cb:(error:null,mime?:string,stream?:NodeJS.ReadableStream)=>void){cb(null,file.mimetype)},

【PHPとNodejs デコレータパターンの実装を比較してみる】

$
0
0

同じようなことをしたい場合この2つの言語でどんな差があるのか気になったので比較してみた。

Node.jsはシンプルにかけるなーと感じました。

PHP

以下のクラス図に沿って実装してみる

36593375-1D87-471B-8BA7-DD339B465525_4_5005_c.jpeg

他のほとんどの言語でもおなじような実装になるとおもう。

Component interface

interfaceComponent{publicfunctionoperation();}

ConcreteComponent class

classconcreteComponentimplementsComponent{publicfunctionoperation(){}}

Decorator class

classDecoratorimplementsComponent{private$component;publicfunction__construct(Component$component){$this->component=$component;}publicfunctionoperation(){}}

ConcreteDecorator class

classconcreteDecoratorextendsDecorator{publicfunction__construct(Component$component){parent::__construct($component);}// デコレートする関数をここに書くpublicfunctionaddedBehavior(){parent::operation();//ここの返り値をデコレートしたりする}}

Node.js

できるだけ上の画像と同じようなイメージで実装してみる。
もとになるcomponentオブジェクトを拡張(デコレート)するときを考える。
Proxyパターンにおけるオブジェクトの拡張も同じように実装できるらしい
注意点は、もとのオブジェクトを上書きしているというところ。

Decorator

exports.decorate=(component)=>{// デコレートする関数component.addedBehavior=()=>{}returncomponent}

component(拡張したいオブジェクト)

component={operation:()=>{}}module.exports=component

concreteDecorator

constcomponent=require('./component')constdecorator=require('./decorator')decorator.decorate(component)console.log(component)//componentにaddedBehavior()が拡張されている

DockerでMySQL・Node.jsコンテナを用いたToDoリストアプリを作成する

$
0
0

はじめに

Dcokerの使い方を学んだので、Docker上でNode.jsアプリケーションを作ってみました。
非常に多くの記事やサイトを参考にして作ったので、自分用のまとめ的な側面もあります。
作成したのは簡単なToDoリストアプリです。

初心者なので色々おかしな点があると思いますが、気づいたらコメントで教えていただけるとありがたいです。

完成イメージ

CRUD機能を搭載した簡単なToDoリストを作ります。
完成イメージは以下の通りです。

ToDoリストアプリの完成形.png

一番上のテキストボックスにタスクを入力し、右にある「add!」ボタンを押すことでタスクを追加できます。(Create)
その下には、既にデータベースに保存されているタスクが表示されます。(Read)
これらのタスクは、チェックボックスやテキストボックスの値を書き換えることで、自動的に更新されます。(Update)
また、タスク右の「delete」ボタンを押すことで、タスクを削除できます。(Delete)

※テキストボックスの値は、Enterキーを押すかフォーカスが外れたときに入力が確定します。

主な参考サイト

基本的には以下のサイトを参考にしていますが、一部自分なりにアレンジしています。

使用したソフトウェア等

DockerホストのPCはWindows10を使用しました。WSL2でUbuntuを実行し、Ubuntu上で作業を行いました。

  • Windows10 バージョン 2004
  • Ubuntu 20.04.2 LTS (Focal Fossa)
  • Docker 20.10.2
  • Node.js 14.16.0
  • express 4.16.1
  • sequelize 6.5.0
  • sequelize-cli 6.2.0
  • VueCLI 4.5.0
  • Vue.js 3.0.0
  • MySQL 8.0.23

システムの全体構成

システムは、3つのコンテナで構成されています。

  • dbコンテナ(MySQLイメージを使用)
    • ToDoリストアプリのデータを保管するデータベースサーバーです。
  • vueコンテナ(Node.jsイメージを使用)
    • ユーザーからのリクエストを受けるサーバーです。今回はフロントエンドにVue.jsを使うため、Vue CLIをインストールします。
  • apiコンテナ(Node.jsイメージを使用)
    • MySQLサーバーを操作するAPIを提供するサーバーです。vueコンテナは本サーバーが提供するAPIを利用してデータベースを操作します。Sequelizeをインストールします。

Docker①.jpg

Docker②.jpg

プロジェクトのディレクトリを作成

ディレクトリの構成は以下のようになります。

  • todo_app
    • docker-compose.yml
    • .env
    • api
      • (expressの雛型ファイル群が入る)
    • vue
      • (Vueの雛型ファイル群が入る)
    • db
      • logs
        • mysql-error.log
        • mysql-query.log
        • mysql-slow.log
      • conf
        • my.cnf

以下のコマンドを適当なディレクトリで実行してください。

mkdir-p todo_app/api
mkdir-p todo_app/vue
mkdir-p todo_app/db/logs
mkdir-p todo_app/db/conf
touch todo_app/docker-compose.yml
touch todo_app/.env
touch todo_app/db/logs/mysql-error.log
touch todo_app/db/logs/mysql-query.log
touch todo_app/db/logs/mysql-slow.log
touch todo_app/db/conf/my.cnf

docker-compose.ymlファイルを編集

todo_app/docker-compose.ymlファイルを以下のように編集します。

todo_app/docker-compose.yml
version:'3'services:db:# 起動するイメージを指定image:mysql:8.0.23# 環境変数を設定environment:-MYSQL_ROOT_HOST=${DB_ROOT_HOST}-MYSQL_DATABASE=${DB_NAME}-MYSQL_USER=${DB_USER}-MYSQL_PASSWORD=${DB_PASS}-MYSQL_ROOT_PASSWORD=${DB_PASS}-TZ=${TZ}# ホスト側のポート:コンテナのポートports:-'3306:3306'# ボリュームバインドvolumes:-./db/conf:/etc/mysql/conf.d/:ro-mysqldata:/var/lib/mysql-./db/logs:/var/log/mysql#使用するネットワークnetworks:-backendapi:image:node:14.16.0-busterenvironment:-MYSQL_SERVER=db-MYSQL_USER=${DB_USER}-MYSQL_PASSWORD=${DB_PASS}-MYSQL_DATABASE=${DB_NAME}-TZ=${TZ}-CHOKIDAR_USEPOLLING=true#コンテナを起動させ続けるよう設定tty:trueports:-'3000:3000'# ソースコードを格納するフォルダをマウント#(ホスト側の./apiをコンテナの/appにマウント)volumes:-./api:/app# 起動時のカレントフォルダを指定working_dir:/app# 起動後に実行するコマンドを指定command:npm run devnetworks:-backend#依存関係(apiコンテナより先にdbコンテナが起動するように設定)depends_on:-dbvue:image:node:14.16.0-busterenvironment:-CHOKIDAR_USEPOLLING=truetty:trueports:-'8080:8080'volumes:-./vue:/appworking_dir:/appcommand:npm run servenetworks:-backenddepends_on:-apinetworks:backend:volumes:mysqldata:

dbapivueの3つのサービスを定義しています。

イメージimage:

イメージはMySQLとNode.jsの公式イメージをそのまま使用しています。(今回はDockerfileを使いません。)Node.jsイメージはnode:14.16.0-busterを使用しています。なお、busterというのはDebianのVer.10のことです。ちなみにDebianのバージョン名は映画「トイ・ストーリー」の登場キャラクターが元になっているそうです。バージョン14.16.0は記事執筆時点での最新のLTS版を使用しています。

環境変数environment:

dbコンテナとapiコンテナには、MySQLを使用(MySQLに接続)するための環境変数を設定していますが、後述する.envファイルで定義したものを取り込むようにしています。直接docker-compose.ymlファイルに記述しなかったのは、データベースの接続情報をソースに含めるのはよろしくないからです。Githubに公開するときは.gitignore.envファイルを追加し、ソース管理から除外しましょう。
apiコンテナとvueコンテナの環境変数には- CHOKIDAR_USEPOLLING=trueを設定しています。これを書かないと、apiコンテナに入れるソースコードの変更を検知して自動でアプリを再起動するライブラリ「nodemon」が正常に動作しません。一応vueコンテナにも書きました。
参考:Docker 環境で nodemon が watch してくれない問題と対処方法 - Qiita

ポートports:

ユーザーからリクエストを受けるvueコンテナの8080番ポートと、ホスト側の8080番ポートを対応させています。
残りの2つのコンテナはコンテナ間のみで通信できればいいので、Dockerホストにマッピングする必要はないですが、apiコンテナもホストから動作を確認したいので設定しています。apiコンテナの3000番ポートと、ホスト側の3000番ポートを対応させています。(なお、dbコンテナも対応付けていますが、これは意味ないです。)

ボリュームvolumes:

dbコンテナでは、ホスト側のtodo_app/db/confディレクトリをコンテナの/etc/mysql/conf.d/ディレクトリにバインドマウントして、MySQLのデフォルト設定を記述したmy.cnfファイル(後述)を上書きしています。
また、MySQLのデータはコンテナが削除されてもデータが消えないように、mysqldataというボリュームを作成し、コンテナの/var/lib/mysqlディレクトリにボリュームマウントしています。(Windows環境ではバインドマウントだと上手くいかないそうです。)
さらに、MySQLのログデータを格納しているディレクトリもバインドマウントすることで、ホストからログを確認できるようにしています。
apiコンテナでは、ホスト側のtodo_app/apiをapiコンテナの/appにバインドマウントすることで、アプリケーションのファイル群をホスト側から編集可能にしています。
vueコンテナでも同様です。(ホスト側のtodo_app/vueをvueコンテナの/appにバインドマウント)

コマンドcommand:

apiコンテナおよびvueコンテナでは、起動時に実行するコマンドを設定しています。しかし、現時点ではこれらのコマンドを動作させるために必要なライブラリがインストールされていないため、コンテナを起動してもすぐに終了してしまいます。

ネットワークnetworks:

コンテナ名でコンテナ間の通信を行うために、backendという名前のDockerネットワークを作り、dbコンテナ、apiコンテナ、vueコンテナで設定しています。

依存関係depends_on:

依存関係を設定しました。dbapivueコンテナの順で起動します。停止するときは逆順になります。vueコンテナ(Todoリストアプリ)はapiコンテナの提供するAPIを使用しないとデータのCRUD(作成・取得・更新・削除)ができません。つまり、vueコンテナはapiコンテナに依存していると言えます。そのapiコンテナも、データをdbコンテナに取りにいかなければなりません。apiコンテナはdbコンテナに依存していると言えます。

my.cnfの編集

todo_app/db/conf/my.cnfを以下のように編集します。

todo_app/db/conf/my.cnf
# MySQLサーバーへの設定
[mysqld]
# 文字コード/照合順序の設定
character-set-server = utf8mb4collation-server = utf8mb4_bin# タイムゾーンの設定
default-time-zone = SYSTEMlog_timestamps = SYSTEM# デフォルト認証プラグインの設定
default-authentication-plugin = mysql_native_password# エラーログの設定
log-error = /var/log/mysql/mysql-error.log# スロークエリログの設定
slow_query_log = 1slow_query_log_file = /var/log/mysql/mysql-slow.loglong_query_time = 5.0log_queries_not_using_indexes = 0# 実行ログの設定
general_log = 1general_log_file = /var/log/mysql/mysql-query.log# mysqlオプションの設定
[mysql]
# 文字コードの設定
default-character-set = utf8mb4# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default-character-set = utf8mb4

ここでは、文字コードの設定や、ログの出力先設定などを行っています。
また、デフォルトの認証プラグインを変更しています。

なお、DockerホストがWindowsの場合、todo_app/db/conf/my.cnfは読み取り専用にしておきます。
参考:[Docker+Windows]mysqlのdockerイメージがmy.cnfのマウントのエラーで起動しない時の対処法 - Qiita

my.cnfを読み取り専用にする①.png

my.cnfを読み取り専用にする②.png

.envファイルの編集

todo.app/.envファイルを以下のように編集します。
docker-compose.ymlでMySQLの環境変数を設定しますが、このファイルから具体的な値を取得しています。
参考:docker-composeのenv_fileと.envファイルの違い - Qiita

todo_app/.env
DB_ROOT_HOST=%
DB_NAME=todo
DB_USER=username
DB_PASS=mypassword
TZ=Asia/Tokyo

apiコンテナを起動してコンテナ内でExpress.jsアプリケーションを作成する

apiコンテナを起動して、express.jsアプリケーションの雛型を作成していきます。
docker-compose.ymlが置いてあるディレクトリ(todo_app/)で次のコマンドを実行します。

#docker-compose.ymlが置いてあるディレクトリに移動cd todo_app

#apiコンテナを一時的に起動$ docker-compose run --rm--no-deps api /bin/bash

このコマンドは、apiコンテナを実行(run)し、シェルを起動する(/bin/bash)という意味になります。
--rmは、実行後に自動でコンテナを削除するオプションです。
--no-depsは、リンクしたサービスを起動しないようにするオプションです。docker-compose.ymlでコンテナの依存関係を定義したため、本来ならばapiコンテナが起動する前にdbコンテナが立ち上がるはずですが、このオプションを設定することでapiコンテナのみ起動するようにしています。

lsコマンドで/appディレクトリ内に何もないことを確認しておきましょう。

====ここからapiコンテナ内 ====# lsコマンドで、/appディレクトリ(=ホスト側のtodo_app/api)内にまだ何もないことを確認ls-al
total 4
drwxrwxrwx 1 node node 4096 Feb 15 11:48 .
drwxr-xr-x 1 root root 4096 Feb 15 11:46 ..

私の環境ではnpmのバージョンを上げておかないとライブラリのインストールが失敗してしまうことがあったので、npmをアップデートしておきます。(今後も何度かこの操作を行うのでDockerfileに書いておくべきだったかもしれません…。)

==== apiコンテナ内 ====# npmのバージョンを確認
npm -v
6.14.11

# npmのアップデート
npm install-g npm

# npmがアップデートされたことを確認
npm -v
7.6.1

npm installでexpress-generatorをインストールして実行します。
(なぜかnpx express-generatorでは上手くいきませんでした。npmのバージョンを上げなければうまくいくのですが…)

==== apiコンテナ内 ====# express-generatorをインストールして実行
npm install-g express-generator
express .

nodemonをインストール。ソースコードを変更したときに、自動でサーバーを再起動してくれる便利なツールです。開発環境でのみ使用するので-D--save-dev)オプションを付けておきます。

==== apiコンテナ内 ====# nodemonをインストール
npm install-D nodemon

exitでコンテナを抜けます。

==== apiコンテナ内 ====# exitでコンテナを抜けるexit====ここまでapiコンテナ内 ====

todo_app/api/package.jsonを編集して、"scripts""dev"を追加します。

todo_app/api/package.json
{"name":"app","version":"0.0.0","private":true,"scripts":{"dev":"nodemon ./bin/www","start":"node ./bin/www"},"dependencies":{"cookie-parser":"~1.4.4","debug":"~2.6.9","express":"~4.16.1","http-errors":"~1.6.3","jade":"~1.11.0","morgan":"~1.9.1","mysql2":"^2.2.5","sequelize":"^6.5.0","sequelize-cli":"^6.2.0"},"devDependencies":{"nodemon":"^2.0.7"}}

以下のコマンドで3つのコンテナをまとめて起動します。

$ docker-compose up -d

起動後、apiコンテナはnpm run devが自動的に実行されます。(docker-compose.ymlで定義しました。)
http://localhost:3000/に接続して、Expressの初期画面が表示されることを確認します。

Expressの初期画面.png

ここで、docker-compose psコマンドでコンテナの起動状況を確認しておきます。

$ docker-compose ps

vueコンテナだけ状態(State)がExitになっています。(停止状態になっています。)

vueコンテナだけExitになっている.png

これは、vueコンテナを起動時に実行されるコマンド(npm run serve)の実行に失敗したためです。後ほどvueコンテナに入りVue CLIをインストールすることで、このコマンドを実行できるようになります。

dbコンテナを起動して正常に設定されていることを確認

起動済みのdbコンテナに入り、MySQLにログインする。

以下のコマンドを実行し、dbコンテナのシェルを起動してMySQLにログインします。

$ docker-compose exec db /bin/bash

====ここからdbコンテナ内 ====
mysql -u root -p

MySQLログイン時のパスワードは、.envファイルで指定したものを入力してください。(今回はmypassword

MySQLの文字コードを確認

MySQLにログイン出来たら、念のため文字コードの設定を確認します。

==== dbコンテナ内 ====
mysql> show variables like 'char%';

MySQL.png

todo_app/db/conf/my.cnfで指定した通りの設定になっていればOKです。

データベースを確認

データベースの状態を確認します。

==== dbコンテナ内 ====
mysql> show databases;

データベースの確認.png

環境変数で指定したtodoというデータベースが存在しているのがわかります。
todoデータベースの中身はどうなっているでしょうか?

==== dbコンテナ内 ====
mysql> use todo;
mysql> show tables;

データベース切り替え&テーブル確認.png

todoデータベースの中身は空です。(テーブルは1つも存在しません。)

テーブルの作成は手作業でもできますが、今回はapiコンテナからSequelizeというライブラリのDBマイグレーション機能を使用して行います。
quitでMySQLを抜け、exitでdbコンテナも抜けてください。

==== dbコンテナ内 ====
mysql> quit
exit====ここまでdbコンテナ内 ====

apiコンテナ内で、sequelizeとその依存パッケージをインストール

今度はapiサーバーを設定していきます。
apiコンテナのシェルを立ち上げ、npmのバージョンをアップデートしておきます。

$ docker-compose exec api /bin/bash
==== apiコンテナ内 ====# npmのバージョンを確認
npm -v
6.14.11

# npmのアップデート
npm install-g npm

# npmがアップデートされたことを確認
npm -v
7.6.1

まず、APIコンテナ内でSequelizeとその依存パッケージをインストールします。

==== apiコンテナ内 ====
npm install mysql2 sequelize sequelize-cli

次に、sequelize-cliを使用してSequelizeの初期化を行います。

==== apiコンテナ内 ====
npx sequelize-cli init

これによって、apiフォルダ内にconfigmigrationsmodelsseedersの4つのディレクトリが作成されます。

次に、以下のコマンドでモデルクラスを生成します。

==== apiコンテナ内 ====
npx sequelize-cli model:generate --name Task --attributes name:string,done:boolean

上記の例では、string型のnameと、boolean型のdoneという2つのプロパティを持つ、Taskというモデルクラスを作成しています。

カラム名説明
namestringタスク名
donebooleanタスクが終了したか否か

実行すると、todo_app/api/modelsディレクトリに、task.jsファイルが出来ているのがわかります。

todo_app/api/models/task.js
'use strict';const{Model}=require('sequelize');module.exports=(sequelize,DataTypes)=>{classTaskextendsModel{/**
         * Helper method for defining associations.
         * This method is not a part of Sequelize lifecycle.
         * The `models/index` file will call this method automatically.
         */staticassociate(models){// define association here}};Task.init({name:DataTypes.STRING,done:DataTypes.BOOLEAN},{sequelize,modelName:'Task',});returnTask;};

また、同時にtodo_app/api/migrationsディレクトリに、{日時}-create-task.jsというファイルも出来ています。

todo_app/api/migrations/[yyyyMMddHHmmss]-create-task.js
'use strict';module.exports={up:async(queryInterface,Sequelize)=>{awaitqueryInterface.createTable('Tasks',{id:{allowNull:false,autoIncrement:true,primaryKey:true,type:Sequelize.INTEGER},name:{type:Sequelize.STRING},done:{type:Sequelize.BOOLEAN},createdAt:{allowNull:false,type:Sequelize.DATE},updatedAt:{allowNull:false,type:Sequelize.DATE}});},down:async(queryInterface,Sequelize)=>{awaitqueryInterface.dropTable('Tasks');}};

この時点のフォルダ構成は次の通りです。

フォルダ構成.png

sequelize-cliでDBマイグレーションを実行

次は、DBマイグレーションを実行してtodoデータベースにテーブルを作成します。
しかし、その前にDB接続情報を正しく設定する必要があります。

先程npx sequelize-cli initでSequelizeの初期化を行った時に、todo_app/api/configディレクトリにconfig.jsonというJSONファイルが出来ているはずですが、それをconfig.jsにしてJavascriptファイルにします。
そして内容を次のように書き換えます。
これは、docker-compose.ymlで設定した環境変数の値をそのままDB接続情報として使うためです。

todo_app/api/config/config.js
module.exports={development:{username:process.env.MYSQL_USER,password:process.env.MYSQL_PASSWORD,database:process.env.MYSQL_DATABASE,host:process.env.MYSQL_SERVER,dialect:'mysql',},test:{username:process.env.MYSQL_USER,password:process.env.MYSQL_PASSWORD,database:process.env.MYSQL_DATABASE,host:process.env.MYSQL_SERVER,dialect:'mysql',},production:{username:process.env.MYSQL_USER,password:process.env.MYSQL_PASSWORD,database:process.env.MYSQL_DATABASE,host:process.env.MYSQL_SERVER,dialect:'mysql',},};

また、todo_app/api/models/index.jsファイルを開き、config.jsonとなっている部分をconfig.jsに変更します。

index.jsの修正.png

この状態で、以下のコマンドを実行するとSequelizeによるDBマイグレーションが実行されます。
つまり、何もなかったtodoデータベースにTasksテーブルが作成されます。

==== apiコンテナ内 ====
npx sequelize-cli db:migrate

最後に、exitでapiコンテナを抜けましょう。

==== apiコンテナ内 ====exit====ここまでapiコンテナ内 ====

Tasksテーブルが作成されたことを確認

todoデータベースにTasksテーブルが作成されたことを確認しておきます。
以下のコマンドでdbコンテナのシェルを実行してください。

docker-compose exec db /bin/bash

先程と同様に、MySQLにログイン後、todoデータベースに切り替え、テーブルを見てみます。

====ここからdbコンテナ内 ====
mysql -u root -p
mysql> use todo
mysql> show tables;

Tasksテーブルの確認.png

結果、Tasksテーブルが出来ていることが確認できました。なお、初回はマイグレーション管理用にSequelizeMetaテーブルも作られます。

ではMySQLとdbコンテナを抜け、ホストに戻ります。

==== dbコンテナ内 ====
mysql> quit
exit====ここまでdbコンテナ内 ====

vueコンテナの準備

ここで、ユーザーからのリクエストを受け取るvueコンテナの準備をしていきます。以前も触れましたが、vueコンテナは起動時に実行されるはずのnpm run serveコマンドが実行できなかったため、停止しています。(docker-compose psでもう一度確認してみましょう。)
ここでは、Vue CLIをインストールし、npm run serveコマンドを使えるようにしていきます。
vueコンテナは停止しているため、docker-compose execは使えません。ここでは、docker-compose runを使ってvueコンテナを一時的に起動します。

docker-compose run --rm--no-deps vue /bin/bash

例によってnpmのバージョンをアップデートしておきます。

==== vueコンテナ内 ====# npmのバージョンを確認
npm -v
6.14.11

# npmのアップデート
npm install-g npm

# npmがアップデートされたことを確認
npm -v
7.6.1

npmのアップデート後、Vue CLIをインストールします。

==== vueコンテナ内 ====
npm install-g @vue/cli

インストール完了後、現在のディレクトリ.を対象にvue createを行います。

==== vueコンテナ内 ====
vue create .

以下の画像を参考に進めていきます。

vue create①.png

↑デフォルトで参照しているレジストリでは接続が遅く、「こっち(https://registry.npm.taobao.org)の方が早いからこっちを使おうよ!」と聞いているようですが、よくわからないのでNo(n)を選択しました。ちなみにtaobaoというのは中国のサーバーらしいです。

vue create②.png

↑カレントディレクトリに作っていいのか聞いています。Yes(Y)で大丈夫です。

vue create③.png

↑プリセットを選択します。Default (Vue 3 Preview) ([Vue 3] babel, eslint)を選びました。

vue create④.png

↑パッケージマネージャーを選択します。NPMを選びました。
その後、待っていればvue createが終わります。(私の環境だと結構時間がかかりました…)

次に、apiコンテナ(APIサーバー)からデータの取得や更新を行うために、axiosをインストールします。

==== vueコンテナ内 ====
npm install axios

インストールが完了したら、exitでvueコンテナを抜けます。

==== vueコンテナ内 ====exit====ここまでvueコンテナ内 ====

そして、docker-compose downで全てのコンテナを停止&削除した後、docker-compose up -dで再び起動し直します。

docker-compose down
docker-compose up -d

ここで、docker-compose psで各コンテナのステータスを確認してみると、今度はvueコンテナも稼働状態になっていることがわかります。Vue CLiをインストールしたことで、npm run serveコマンドが実行できるようになりました。

docker-compose ps

vueコンテナもUpになっている.png

http://localhost:8080/に接続し、Vue.jsの初期画面が表示されることを確認します。

Vue.jsの初期画面.png

APIの実装

ここからは、apiコンテナ内のプログラムを編集し、ToDoリストアプリのCRUDのAPIを実装していきます。
まず、todo_app/api/routes/index.jsに2行追加します。
/taskルートの処理をtaskRoutes.jsに投げるようにします。

todo_app/api/routes/index.js
varexpress=require('express');varrouter=express.Router();//この2行を追加するconsttaskRoutes=require("./taskRoutes");router.use("/task",taskRoutes);/* GET home page. */router.get('/',function(req,res,next){res.render('index',{title:'Express'});});module.exports=router;

次に、todo_app/api/routes/に、ファイルtaskRoutes.jsを作成して、以下のように編集します。

todo_app/api/routes/taskRoutes.js
"use strict";constrouter=require("express").Router(),taskController=require("../controllers/taskController");router.get("/",taskController.read);router.post("/",taskController.create);router.put("/:id",taskController.update);router.delete("/:id",taskController.delete);module.exports=router;

ここでは、4つのhttpメソッド(get、post、put、delete)毎に違う処理を割り当てています。
処理の内容は、todo_app/api/controllers/taskController.js内に記述していきます。

todo_app/api/に、controllersという名前のディレクトリを作成し、その中にtaskController.jsを作成します。
todo_app/api/controllers/taskController.jsは、以下のように編集します。

todo_app/api/controllers/taskController.js
"use strict";constdb=require("../models/index");module.exports={read:async(req,res,next)=>{try{constresult=awaitdb.Task.findAll();res.send(result);}catch(err){res.status(500).send(err);}},create:async(req,res,next)=>{try{constresult=awaitdb.Task.create({name:req.body.name,done:false});res.send(result);}catch(err){res.status(500).send(err);}},update:async(req,res,next)=>{try{constresult=awaitdb.Task.update({name:req.body.name,done:req.body.done},{where:{id:req.params.id}});res.send(result);}catch(err){res.status(500).send(err);}},delete:async(req,res,next)=>{try{constresult=awaitdb.Task.destroy({where:{id:req.params.id}});res.send({result:result});}catch(err){res.status(500).send(err);}}}

これで、APIコンテナの実装は完了です。

Vue.jsの実装

最後に、フロントエンドの実装を行います。
デフォルトで存在するtodo_app/vue/src/components/HelloWorld.vueというコンポーネントを以下のように書き換えます。

todo_app/vue/src/components/HelloWorld.vue
<template><divclass="hello"><form><inputtype="text"style="display: none"/><inputv-model="currentTask"type="text"/><inputtype="button"value="add!"@click="taskCreate"/></form><tablealign="center"border="0"><tr><th>done</th><th>task</th><th>delete</th></tr><trv-for="(task, index) in tasks":key="task.id"><td><inputtype="checkbox"v-model="task.done"@change="taskUpdate(task.id, task.name, task.done)"/></td><td><inputtype="text"v-model="task.name"@change="taskUpdate(task.id, task.name, task.done)"/></td><td><inputtype="button"value="delete"@click="taskDelete(task.id, index)"/></td></tr></table></div></template><script>importaxiosfrom"axios";exportdefault{name:"HelloWorld",data:()=>({tasks:[],currentTask:"",}),created:asyncfunction(){try{constresult=awaitaxios.get("/task/");this.tasks=result.data;}catch(err){alert(JSON.stringify(err));}},methods:{taskCreate:asyncfunction(){try{constresult=awaitaxios.post("/task/",{name:this.currentTask,});this.tasks.push(result.data);this.currentTask="";}catch(err){alert(JSON.stringify(err));}},taskDelete:asyncfunction(id,index){try{awaitaxios.delete("/task/"+id);this.currentTask="";this.tasks.splice(index,1);}catch(err){alert(JSON.stringify(err));}},taskUpdate:asyncfunction(id,val,done){try{awaitaxios.put("/task/"+id,{name:val,done:done,});this.currentTask="";}catch(err){alert(JSON.stringify(err));}},},};</script><!-- Add "scoped" attribute to limit CSS to this component only --><stylescoped>h3{margin:40px00;}ul{list-style-type:none;padding:0;}li{margin:010px;}a{color:#42b983;}.table{height:100%;text-align:center;}</style>

次に、todo_app/vue/にファイルvue.config.jsを作成し、以下のように編集します。
参考:Vue.jsとAPIサーバとのaxiosでCORSに引っかかった時のProxyを使った回避方法 - Qiita

todo_app/vue/vue.config.js
module.exports={devServer:{proxy:'http://api:3000'}};

編集し終わったら、ここで各コンテナを再起動しましょう。

docker-compose down
docker-compose up -d

これで、ToDoリストアプリが完成しているはずです。http://localhost:8080/にアクセスして操作してみましょう。

ToDoリストアプリの完成形.png

出来ました!

参考書籍

主な参考サイト

Node.jsをOpenTelemetryでメトリック収集してみる

$
0
0

はじめに

前回、Node.js(JavaScript)でOpenTelemetryによるトレースデータ収集を行なったので、今度はカウンタ値のようなメトリックデータ収集についてまとめてみました。

OpenTelemetryの概要やトレースデータ収集の記事はこちらを参照。

やってみること

Node.jsアプリケーション上で収集したメトリックデータ(今回は3種類のカウンタの数値)を、Prometheusでグラフ化してみます。
architecture.png

実行環境

  • MacOS X
  • node: v13.8.0
  • npm: v7.5.3
  • express: v4.17.1
  • OpenTelemetry: v0.18

1. サンプル構成の作成

まずはベースとなるサンプルアプリケーションを作成します。

サンプルの内容は、URLにアクセスすると3種類の色(red, blue, yellow)のいずれかを返す単純なロジックとなります。

1.1 サンプル作成

プロジェクトにExpressをインストールします。

$ npm install express

アプリケーション(app.js)を作成します。

app.js
'use strict';constexpress=require('express');constapp=express();constPORT=process.env.PORT||'8080';app.use(function(req,res,next){// ランダムで色を選ぶconstcolor=['red','blue','blue','yellow','yellow','yellow'];res.locals.color=color[Math.floor(Math.random()*color.length)];next();});app.get('/',(req,res,next)=>{res.send(res.locals.color);next();});app.listen(parseInt(PORT,10),()=>{console.log(`Listening for requests on http://localhost:${PORT}`);});

1.2 サンプルの動作確認

サンプルを実行すると、localhost:8080でクライアントからのリクエストを待ち受けるようになります。

$ node app.js
Listening for requests on http://localhost:8080

別のターミナルからサンプルのURLにアクセスします。

$ curl localhost:8080
red

上記のようにred, blue, yellowのいずれかが返却されます。

2. Prometheusの起動

メトリック収集の準備として、メトリックデータのグラフ化を行うPrometheusを構築します。

今回はDockerコンテナ版を使用します。

コンテナ起動前に、まずprometheus.ymlを作成します。

global:scrape_interval:15sscrape_configs:-job_name:'opentelemetry'static_configs:-targets:-'host.docker.internal:9464'

targetsはPrometheusにおけるメトリックデータの収集先となります。
今回はホストとなりますが、コンテナから見たホストはlocalhostではなくhost.docker.internalとなります(localhostはコンテナを指す)。
なおhost.docker.internalはLinuxでは使えないため(MacやWindowsなら可)、Linux環境の場合はホストのIPアドレスに置き換えてください。

docker run \-d\-p 9090:9090 \-v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml \
    prom/prometheus

localhost:9090にアクセスし、PrometheusのGUI画面が表示されたら起動OKです。

prometheus1.png

3. OpenTelemetryの組み込み

それでは先ほど作成したサンプルに、メトリックデータ収集を行うOpenTelemetryを組み込んでいきます。

3.1 パッケージのインストール

$ npm install @opentelemetry/metrics @opentelemetry/exporter-prometheus

3.2 monitoring.jsの作成

サンプルに組み込むカウンタのロジックを作成します。

monitoring.js
'use strict';const{MeterProvider}=require('@opentelemetry/metrics');const{PrometheusExporter}=require('@opentelemetry/exporter-prometheus');constprometheusPort=PrometheusExporter.DEFAULT_OPTIONS.portconstprometheusEndpoint=PrometheusExporter.DEFAULT_OPTIONS.endpointconstexporter=newPrometheusExporter({startServer:true,},()=>{console.log(`prometheus scrape endpoint: http://localhost:${prometheusPort}${prometheusEndpoint}`,);},);constmeter=newMeterProvider({exporter,interval:1000,}).getMeter('color-meter');constcolorCount=meter.createCounter('colors',{description:'Count each color'});// 色ごとにCounterを生成するconstredCounter=colorCount.bind({color:'red'});constblueCounter=colorCount.bind({color:'blue'});constyellowCounter=colorCount.bind({color:'yellow'});module.exports.countColorRequests=()=>{return(req,res,next)=>{switch(res.locals.color){case'red':redCounter.add(1);break;case'blue':blueCounter.add(1);break;case'yellow':yellowCounter.add(1);break;}next();};};

カウンタはmeter.createCounter()で作成します。

引数で指定した文字列が、メトリックデータのテーブル名に相当します。

また今回は返却した色ごとのカウンタ値をグラフ化したいので、以下のようにmeter.createCounter()で作成したカウンタにラベルをbindし、3色分のカウンタを作成します。
ここで指定する color: 'red'はグラフの系列名に相当します。

const redCounter = colorCount.bind({ color: 'red'});

3.3 サンプルへの組み込み

先ほど作成したmonitoring.jsをサンプルに組み込みます。

app.js
'use strict';constexpress=require('express');constapp=express();constPORT=process.env.PORT||'8080';+ const{countColorRequests}=require("./monitoring");app.use(function(req,res,next){// ランダムで色を選ぶconstcolor=['red','blue','blue','yellow','yellow','yellow'];res.locals.color=color[Math.floor(Math.random()*color.length)];next();});+ app.use(countColorRequests());app.get('/',(req,res,next)=>{res.send(res.locals.color);next();});app.listen(parseInt(PORT,10),()=>{console.log(`Listening for requests on http://localhost:${PORT}`);});

Math.random()で色を選択した後、countColorRequests()を呼び出し、選択した色のカウンタをインクリメントしています。

3.4 動作確認

1章と同様に、サンプルを起動します。
起動すると、localhost:8080のリクエスト待ち受けに加え、localhost:9464/metricsでもPrometheusからのメトリックデータ収集(Pull)を待ち受けるようになります。

$ node app.js 
Listening for requests on http://localhost:8080
prometheus scrape endpoint: http://localhost:9464/metrics

サンプルへのリクエスト送信を100回連続で行い、カウンタ値を増やしてみます。

$ for var in `seq 100`; do curl localhost:8080; done

カウンタ値は、localhost:9464/metricsにアクセスすると参照できます。
(Prometheusはこの内容を定期的に収集します)

$ curl http://localhost:9464/metrics
# HELP colors Count each color# TYPE colors counter
colors{color="red"} 14 1615214799584
colors{color="blue"} 32 1615214799558
colors{color="yellow"} 54 1615214799597

それではPrometheusでグラフ表示してみます。

Graphタブを選択し、テキストボックスにcolorsと入力、Executeボタンを実行します。
すると、下図のようなグラフが出力され、カウンタ値のインクリメントの推移を確認することができます。

prometheus.png

おわりに

メトリック収集はOpenTelemetryを使わなくてもさまざまな方法で実現できますが、ユーザアプリケーション独自のメトリックを収集する場合にOpenTelemetryは強みを発揮しそうです。

OpenTelemetryについて、他にも面白そうな機能があれば試していきたいと思います。

nodeでrequireが使えない場合の対処

$
0
0

現象

node実行(※ブラウザ実行ではない)にもかかわらず、
ファイル読み込み関数requireが使えずに下記エラーが出る。

require is not defined node

調査結果

NodeV14以降のmoduleではrequire使えないらしい。
下記のようにインポートすればOK。

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

参照

As Abel said, ES Modules in Node >= 14 no longer have require by default.

If you want to add it, put this code at the top of your file:

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

Node.jsでUNIX domain socketを使ってhttpサーバを立ち上げる方法

$
0
0

事前準備

npm i -g typescript ts-node node-fetch 

コード

// サーバーサイドコードimporthttpfrom'http'importfsfrom'fs'constsocketPath='/tmp/http-test.sock'// socketファイルがあると立ち上がらないため消しとくfs.existsSync(socketPath)&&fs.unlinkSync(socketPath)constserver=http.createServer((_,res)=>{res.statusCode=200res.setHeader('Content-Type','text/plain')res.end('Hello, World!\n')})// ちなみにexpressでも、下記listen方法で動きます。server.listen(socketPath,()=>{console.log('Example app listening on socket',socketPath)})

# curlで叩く場合
curl -v--unix-socket /tmp/http-test.sock http:/index.html

// クライアントコードの場合importhttpfrom'http'importfetchfrom'node-fetch'constsocketPath='/tmp/http-test.sock'construn=async():Promise<void>=>{constagent=newhttp.Agent({socketPath})// 動くのだが、tsのチェックがキツイとエラー扱い。。。constres=awaitfetch('http://localhost/',{agent})console.log(res.status)}run().catch(console.error)

コメント

上記で動きます。

注意点としては、node-fetchを使ったクライアントコードがtypescriptで厳し目の設定の場合エラーとなることです。

出典:node-fetch-issues-336

エラーになって実行できない場合、下記でチェックをスキップして、トランスパイルのみで実行出来ます。

ts-node --transpile-only client.js

クライアントコードは、2021/3/10時点で動いてますが、issueではバグとかなんとか言っているので、この書き方はいずれ動かなくなるかもしれません。

まぁ、curl動けば確認できますし、仕様が変更になったらコードを変更すればいいだけですから、そんなでもないですね。

[Node.js] 非同期処理 - コールバック編

$
0
0

コールバック

コールバックを利用する非同期プログラミングは、JavaScriptにおける非同期プログラミングの実装パターンとして最も基本的なものです。このパターンでは、非同期に処理を行う関数に引数として渡したコールバックが処理完了時に実行されます。

まずは簡単に"setTimeout()"を使用したコールバックを実行してみます。

setTimeout(()=>console.log('1秒通過しました'),1000);console.log('setTimeout()を実行しました。');>>>setTimeout()を実行しました1秒通過しました

実行すると、「setTimeout()」の処理を待たずに「console.log()」が実行されている。
コールバックは自動機のインターフェースとして用いられるが、コールバックは必ず非同期処理というわけではない。

constarray1=[0,1,2,3,4]constarray2=array1.map(v=>{console.log(`${v}を変換します。`)returnv*10});console.log(array2)>>>0を変換します1を変換します2を変換します3を変換します4を変換します[0,10,20,30,40]

エラーハンドリング

JavaScriptではエラーハンドリングのためにtry...catch構文が用意されています。
try...catchではコールバックの中で発生したエラーをハンドリングできない。

constparseJSONAsync=(json,next)=>{try{setTimeout(()=>next(JSON.parse(json)),1000);}catch(err){console.log('エラーをキャッチしました。',err);next({})}}parseJSONAsync('json',result=>console.log('parse結果',result))>>>SyntaxError:UnexpectedtokenjinJSONatposition0

Node.jsは通常、エラーがイベントループまで到達するとprocessオブジェクトからuncaughtExceptionいベントが発行され、アプリケーションが停止します。uncaughtExceptionを補足し、これを握りつぶしてアプリケーションの停止を食い止めることも可能ですが、この結果アプリケーションの整合性が保証されない状態になるため推奨されません。

process.on('uncaughtException',err=>process.exit(1))

コールバックの中では起こりうるアプリケーションエラーを適切にキャッチし、それをイベントループまで到達させることなく呼び出し元に返すことが重要です。

constparseJSONAsync=(json,next)=>{setTimeout(()=>{try{next(null,JSON.parse(json));}catch(err){next(err)}},1000)}parseJSONAsync('json',(err,result)=>console.log('parse結果',err,result))>>>parse結果SyntaxError:UnexpectedtokenjinJSONatposition0

同期と非同期を混ぜると危険

同じ文字列に対してJSONAsync()を実行した結果は常に同じになるはずですから、この結果をキャッシュして使い回してみます。

constcache={}constparseJSONAsyncWithCache=(json,callback)=>{constcached=cache[json]if(cached){callback(cached.err,cached.result)return}parseJSONAsync(json,(err,result)=>{cache[json]={err,result}callback(err,result)})}parseJSONAsyncWithCache('{"message": "Hello", "to": "world"}',(err,result)=>{console.log('1回目の結果',err,result)parseJSONAsyncWithCache('{"message": "Hello", "to": "world"}',(err,result)=>console.log('2回目の結果',err,result))console.log('2回目の呼び出し完了')})console.log('1回目の呼び出し完了')>>>1回目の呼び出し完了1回目の結果null{message:'Hello',to:'world'}2回目の結果null{message:'Hello',to:'world'}2回目の呼び出し完了

この実装は、JavaScriptのよく知られたアンチパターンです。ここでの問題は、parseJSONAsyncWithCache()が状況によってコールバックを同期的に実行したり非同期的に実行したりすることです。
コールバックの呼び出しが同期的か非同期かで一貫性がないと、APIの挙動が予期しづらくなってしまいます。今回の例では1回目の呼び出しでは"console.log('1回目...')"の後にコールバック関数が実行されているのに対して、2回目はその順番が逆になっています。状況によってこうした順番が前後するのは、複雑で原因の特定が困難な不具合の原因になりかねない。
これを書き直すと次のようになる。

constcache2={}constparseJSONAsyncWithCache=(json,callback)=>{constcached=cache2[json]if(cached){// キャッシュに値が存在する場合でも、非同期的にコールバックを実行するsetTimeout(()=>callback(cached.err,cached.result),0)return}parseJSONAsync(json,(err,result)=>{cache2[json]={err,result}callback(err,result)})}parseJSONAsyncWithCache('{"message": "Hello", "to": "world"}',(err,result)=>{console.log('1回目の結果',err,result)parseJSONAsyncWithCache('{"message": "Hello", "to": "world"}',(err,result)=>console.log('2回目の結果',err,result))console.log('2回目の呼び出し完了')})console.log('1回目の呼び出し完了')>>>1回目の呼び出し完了1回目の結果null{message:'Hello',to:'world'}2回目の呼び出し完了2回目の結果null{message:'Hello',to:'world'}

これで1回目も2回目も一貫性が貯めたれるようになりました。
非同期的に行っている"setTimeout"を"process.nextTick()"に置き換えても問題ありません。
process.nextTick()はsetTimeout()よりもはやくコールバックを実行するため、すぐに値を返したいという要求により適している。
ただし、process.nextTick()はブラウザ環境のJavaScriptには存在しないAPIのため、ブラウザ上でも動かすコードを書いている場合には使えない。
ブラウザと共有して使用する場合には、Web標準に由来するグローバルメソッドのqueueMicrotask()を利用するとよい。

コールバックヘル

複数の非同期処理を逐次的に実行する場合、非同期処理のコールバックの中で次の非同期処理を実行する必要があります、これを繰り返した結果、コードのネストが深く可読性や保守性が損なわれる状態になります。この状態をコールバックヘルと呼びます。これはJavaScriptの最もよく知られたアンチパターンです。

asyncFunc1(input,(err,result)=>{if(err){...}asyncFunc2(input,(err,result)=>{if(err){...}asyncFunc3(input,(err,result)=>{if(err){...}})})})

解決策としては、コードを分割することです。

functionfirst(arg,callback){asyncFunc1(input,(err,result)=>{if(err){...};second(result,callback);});}functionsecond(arg,callback){asyncFunc2(input,(err,result)=>{if(err){...};third(result,callback);});}functionthird(arg,callback){...}

nodeでマルチキャストを送信する

$
0
0

node.jsでmulticast

色々調べたけど、うまくいくサンプルがなかったので、書きました。
setMulticastInterfaceをちゃんと入れるのがポイント。

varnews=["Borussia Dortmund wins German championship","Tornado warning for the Bay Area","More rain for the weekend","Android tablets take over the world","iPad2 sold out","Nation's rappers down to last two samples"];constHOST_ADDRESS="192.168.0.1";constMULTICAST_ADDRESS="230.255.192.1"constPORT=5000;vardgram=require('dgram');varserver=dgram.createSocket("udp4");server.on("listening",function(){server.setBroadcast(true);server.setMulticastTTL(128);server.addMembership(MULTICAST_ADDRESS,HOST_ADDRESS);server.setMulticastInterface(HOST_ADDRESS);// これがないとIFからパケットが出なかった});server.bind();setInterval(broadcastNew,3000);functionbroadcastNew(){varmessage=newBuffer(news[Math.floor(Math.random()*news.length)]);server.send(message,0,message.length,PORT,MULTICAST_ADDRESS);console.log("Sent "+message+" to the wire...");//server.close();}

参考

gulp, webpack, Babelを使った環境構築(on WordPress)

$
0
0

Node.jsを構築する

Node.jsのプロジェクトを構築します.

前提

Linux環境またはWindows10のWSL

Node.jsインストール

Node.jsがインストールされていない場合はまずインストールする.
LTSのバージョンがおすすめです.
Node.jsのサイトに行ってダウンロードしてもいいし,
下記のようにパッケージ管理ツールからインストールしてもいいです.
Windows環境でPowerShellで使ったりする場合はインストーラーでインストールするのがいいと思います.

WSLの場合は

$curl-sL https://rpm.nodesource.com/setup_14.x | bash -
$aptinstall-y nodejs

または

wget https://nodejs.org/dist/v14.15.4/node-v14.15.4-linux-x64.tar.xz
tar-xvf node-v14.15.4-linux-x64.tar.xz
cp node-v14.15.4-linux-x64/bin/node /bin/node
rm-rf node-v14.15.4-linux-x64
rm-rf node-v14.15.4-linux-x64.tar.xz

等で直接配置してもいいです.

npmインストール

Node.js製のパッケージ管理ツール(これがないと始まらない).
Node.jsが入った状態で

$ node install npm

Docker等で仮想化していない場合はバージョンを色々変えたいときがあろうかと思うので,
バージョン管理ツールをインストールしておきましょう. 主に下記のようなものがあります.
- nvm : https://github.com/nvm-sh/nvm

$npminstall nvm
$npminstall n

npm installとpackage.json

package.jsonがあるディレクトリで

#babel$npminstall

と打てば依存関係の全てのパッケージが自動でインストールされます.

ない場合は下記の通り.

ローカルのNode.js環境の初期構築

とりあえず, sass compile, babelのコンパイルなどを使うなら以下の通り

gulp

Node製のタスクランナー, 設定ファイルがjsで書かれている.

$ cd /path/to/project/root
$npminstall-D gulp
#その他必要なパッケージは適宜インストールする

プロジェクトルートディレクトリにgulpfile.js (babelを使う場合はgulpfile.babel.js)を作成し, 以下のように記述していく.

constthemeName='seiaikai-southerncross.com';//vargulp=require('gulp');varplumber=require('gulp-plumber');varrename=require('gulp-rename');varsass=require('gulp-sass');varcsslint=require('gulp-csslint');varautoPrefixer=require('gulp-autoprefixer');//if node version is lower than v.0.1.2require('es6-promise').polyfill();varcssComb=require('gulp-csscomb');varcmq=require('gulp-merge-media-queries');varcleanCss=require('gulp-clean-css');varuglify=require('gulp-uglify');varconcat=require('gulp-concat');varmerge=require('merge-stream');//webpackvarwebpack=require('webpack');varwebpackStream=require('webpack-stream');varwebpackConfig=require('./webpack.config.js');constpaths={css:{main:`./httpdocs/cms/wp-content/themes/${themeName}/css/main.css`,pages:`./httpdocs/cms/wp-content/themes/${themeName}/css/pages`},scss:{main:`./httpdocs/cms/wp-content/themes/${themeName}/css/main.scss`,pages:`./httpdocs/cms/wp-content/themes/${themeName}/css/pages/*.scss`,},js:{rootDir:`./httpdocs/cms/wp-content/themes/${themeName}/js`,index:`./httpdocs/cms/wp-content/themes/${themeName}/js/index.js`,}}gulp.task('sass',function(done){// main.scssvarmain=gulp.src([paths.scss.main]).pipe(plumber({handleError:function(err){console.log(err);this.emit('end');}})).pipe(sass()).pipe(autoPrefixer()).pipe(cmq({log:true})).pipe(gulp.dest(paths.css))//pages sass compilevarpages=gulp.src([paths.scss.pages]).pipe(plumber({handleError:function(err){console.log(err);this.emit('end');}})).pipe(sass()).pipe(autoPrefixer()).pipe(cmq({log:true})).pipe(gulp.dest(paths.css.pages))returnmerge(main,pages);});gulp.task("webpack",function(done){webpackStream(webpackConfig,webpack).pipe(gulp.dest(paths.js.rootDir));done();});gulp.task('sass:watch',function(){gulp.watch(`httpdocs/cms/wp-content/themes/${themeName}/css/*.scss`,gulp.task('sass'));gulp.watch(`httpdocs/cms/wp-content/themes/${themeName}/css/*/*.scss`,gulp.task('sass'));gulp.watch(`httpdocs/cms/wp-content/themes/${themeName}/js/*/*.js`,gulp.task('babel'));gulp.watch(`httpdocs/cms/wp-content/themes/${themeName}/js/*.js`,gulp.task('babel'));done();});

この状態でプロジェクトルート(gulpfileがあるディレクトリ)で

$npx gulp タスクの名前(i.e. sass or babel)

などと打つと実行されます

webpack

#webpack$npminstall-D webpack webpack-stream terser-webpack-plugin \
webpack-cli @webpack-cli/init
#その他必要なパッケージは適宜インストールする

ここでは数々のjsファイルをindex.jsにexportで集めて, index.bundle.js に書き出すこととします.
ディレクトリ構造は

js/modules/
js/pages/
js/index.js
js/index.babel.js
などとしておきます.

constthemeName='seiaikai-southerncross.com';constpath=require('path');consturl=require('url');constwebpack=require('webpack');constTerserPlugin=require('terser-webpack-plugin');constpaths={url:"",js:{rootDir:`httpdocs/cms/wp-content/themes/${themeName}/js`,index:`httpdocs/cms/wp-content/themes/${themeName}/js/index.js`,bundle:`httpdocs/cms/wp-content/themes/${themeName}/js/index.bundle.js`},}//master branch の場合は mode: "production" としてpushしてデプロイする.module.exports={mode:'development',entry:{index:path.resolve(__dirname,paths.js.index)//相対パスの場合//index: path.join(__dirname, 'httpdocs', 'cms', 'wp-content', 'themes', `${themeName}`, 'js','index.js')},output:{filename:path.join('[name].bundle.js'),path:path.join(__dirname,paths.js.rootDir)},module:{rules:[{test:/\.js$/,use:[{loader:'babel-loader',options:{presets:[['@babel/preset-env',{'modules':false}]]}}]}]},devtool:'source-map',optimization:{minimizer:[// js圧縮newTerserPlugin({extractComments:'all',// コメント削除terserOptions:{compress:{drop_console:false,// console.log削除 boolean},},}),],}};

babel [#z40190d3]

#babel$npminstall-D webpack babel-loader @babel/core @babel/preset-env babel-preset-env
#その他必要なパッケージは適宜インストール

参考

一次ソース

https://gulpjs.com/
https://webpack.js.org/
https://babeljs.io/

日本語の参考記事

https://webpack.js.org/loaders/babel-loader/
https://qiita.com/KoichiSugimoto/items/d8a5563f197682dea0f4
https://qiita.com/bakira/items/3c4e2d10aae085767817
https://qiita.com/am10/items/2516fa04def815195ffe
https://qiita.com/tmiki/items/86abc565d06ced78d968

CentOS 6.10でNode.js 14.16(LTS)を使用可能にする(gitアップデート, nvmインストール編)

$
0
0

現状

サポートの終わったCentOS6.10のレガシーなサーバーのネイティブ環境でNode.jsがどうしても使いたい。
とりあえず nvm経由でインストールしてみる。

nvmインストール

こちらから

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash

すると

=> Unknown option: -c
usage: git [--version][--exec-path[=GIT_EXEC_PATH]] [--html-path][-p|--paginate|--no-pager] [--no-replace-objects][--bare][--git-dir=GIT_DIR] [--work-tree=GIT_WORK_TREE]
           [--help] COMMAND [ARGS]
Failed to clone nvm repo. Please report this!

どうやらgit commandに -cがないようなのでgit --versionを確認

$ git --version
git version 1.7.1

古すぎるのでアップデートします。

gitのアップデート

$ sudo yum remove git
$ sudo yum install gcc curl-devel expat-devel gettext-devel openssl-devel zlib-devel perl-ExtUtils-MakeMaker
$ wget https://www.kernel.org/pub/software/scm/git/git-2.2.0.tar.gz
$ tar-zxf git-2.2.0.tar.gz
$ cd git-2.2.0
$ make prefix=/usr/local all
$ make prefix=/usr/local install

しばらく待つと, 終わるので git --versionを確認

rm-f"$execdir/$p"&&\test-z""&&\ln"$execdir/git""$execdir/$p" 2>/dev/null ||\ln-s"git""$execdir/$p" 2>/dev/null ||\cp"$execdir/git""$execdir/$p"||exit;\done&&\remote_curl_aliases="git-remote-https git-remote-ftp git-remote-ftps"&&\for p in$remote_curl_aliases;do\rm-f"$execdir/$p"&&\test-z""&&\ln"$execdir/git-remote-http""$execdir/$p" 2>/dev/null ||\ln-s"git-remote-http""$execdir/$p" 2>/dev/null ||\cp"$execdir/git-remote-http""$execdir/$p"||exit;\done&&\
        ./check_bindir "z$bindir""z$execdir""$bindir/git-add"$ git --version-bash: /usr/bin/git: そのようなファイルやディレクトリはありません

と怒られる。

https://qiita.com/sirone/items/2e233ab9697a030f1335
にある通り,

gitを実行しようとした時に「そんなファイルはないよ!」と指摘されるファイルパスと、whichtypeで確認したときのファイルパスで、ズレが生じるケースがあるようです。

$ git --version-bash: /usr/bin/git: No such file or directory

$ which git
/usr/local/bin/git

$ type git
git is /usr/local/bin/git

この場合、bashの組み込みコマンドである hashコマンドで解決することが出来る、とのことでした。

$ hash-r$ git --version
git version 2.2.0

インストールが確認できた.

nvmインストール(2回目)

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash

vimで.bashrcに追記

export NVM_DIR="$([-z"${XDG_CONFIG_HOME-}"]&&printf %s "${HOME}/.nvm"||printf %s "${XDG_CONFIG_HOME}/nvm")"[-s"$NVM_DIR/nvm.sh"]&&\."$NVM_DIR/nvm.sh"# This loads nvm

バージョンを確認

$ nvm -v
0.37.2

nvmがインストールできました。

LTSバージョンをnvm経由でインストール(2021年3月現在)

$ nvm install--lts
Installing latest LTS version.
Downloading and installing node v14.16.0...
Downloading https://nodejs.org/dist/v14.16.0/node-v14.16.0-linux-x64.tar.xz...
######################################################################## 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v14.16.0 (npm v)
Creating default alias: default -> lts/*(-> v14.16.0)

ここでnodeコマンドを打つと,

$ node
node: /lib64/libc.so.6: version `GLIBC_2.16' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.17' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by node)
node: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.14' not found (required by node)
node: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.18' not found (required by node)
node: /usr/lib64/libstdc++.so.6: version `CXXABI_1.3.5' not found (required by node)
node: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.15' not found (required by node)
[webadmin@www6108ug ~]$ node -v
node: /lib64/libc.so.6: version `GLIBC_2.16' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.17' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by node)
node: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.14' not found (required by node)
node: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.18' not found (required by node)
node: /usr/lib64/libstdc++.so.6: version `CXXABI_1.3.5' not found (required by node)
node: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.15' not found (required by node)

どうやらlibc, libstdc++などが古すぎて対応しておらず nodeコマンドが使えないようです。

これを解決するにはgccのビルドと必要なライブラリをインストールする必要があります。
これに関してはまた次回書きます。

参考

[Node.js] 非同期処理 - Promise編

$
0
0

ES2015で導入された非同期処理の状態と結果を表現するオブジェクトです。
Promiseを利用した非同期関数の実装では、関数は呼び出されるとその場ですぐPromiseインスタンスを返す必要がありますが、処理の結果を確定するのはあとから(非同期)です。結果が未確定のPromiseインスタンスの状態をpendingといい、非同期処理に成功した状態をfulfilledと呼ぶ。一度でも"fulfilled"または"rejected"になったPromiseインスタンスの状態はそれ以降変化せず、これらの状態をsettledと総称します。

functionparseJSONAsync(json){returnnewPromise((resolve,reject)=>{setTimeout(()=>{try{resolve(JSON.parse(json))}catch(err){reject(err)}},1000)})}

Promiseのコンストラクタを使ってPromiseインスタンスを作っています。Promiseのコンストラクタは関数をぱらめーたとし、この関数はresolve, rejectという2つの関数を引数として実行されます。コンストラクタが実行された時点ではこのPromiseインスタンスの状態はpendingです。処理結果を引数にresolve()を実行するとfulfilledになり、エラーを引数にreject()を実行すると、"rejected"になります。
※ resuleve()の引数は必須ではなく、引数なしで実行するとからの値を持ったPromiseになる
※ reject()の引数も同様に非必須で、かつ任意の値を利用できるが、通常はErrorのインスタンスを引数に渡す

consttoBeFulfilled=parseJSONAsync('{"foo": 1}');consttoBeRejected=parseJSONAsync('不正なJSON');console.log('****** Promise生成直後 ******');console.log(toBeFulfilled);console.log(toBeRejected);setTimeout(()=>{console.log('******* 1秒後 *******');console.log(toBeFulfilled);console.log(toBeRejected);},1000)>>>******Promise生成直後******Promise{<pending>}Promise{<pending>}(node:9042)UnhandledPromiseRejectionWarning:SyntaxError:UnexpectedtokeninJSONatposition0atJSON.parse(<anonymous>)...(省略)*******1秒後*******Promise{{foo:1}}Promise{<rejected>SyntaxError:UnexpectedtokeninJSONatposition0atJSON.parse(<anonymous>)...(省略)}

生成した2つのPromiseインスタンスの状態は最初どちらもpendingです。1秒経過すると正常なJSONを渡した方はそのパース結果を保持したfulfilled状態となり、不正なJSONを渡した方はrejected状態になります。

Promiseインスタンスの状態以外に注目すべき点は、UnhandledPromiseRejectionWarningが出力されています。これはrejected状態になったPromiseインスタンスに対して、イベントループが次のフェーズに進むまでエラーハンドリングが行われなかった場合に出力される警告です。この時、processオブジェクトがUnhandledRejectionイベントを発行します。UncaughtExceptionと異なり、このイベントはREPLでも発行され、デフォルトではこのイベントが発行される状況でもNode.jsのプロセスが落ちることはありません。

process.on('UnhandledRejection',(err,promise)=>console.log('UnhandledRejection発生',err));

UnhandledPromiseRejectionWarningで述べられているとおり、UnhandledRejectionが発行されている状況でNode.jsのプロセスを終了させたい場合は、nodeコマンド実行時に、--unhandled-rejections=strictを指定します。
そのほかにprocess.exit(1);などを使ってプロセスをエラー終了する方法もあります。

pendingを経ず、fulfilledまたはrejectedなPromiseインスタンスを直接生成したい場合、Promiseのコンストラクタも使えるが、より簡単な手段として、Promise.resolve(), Promise.reject()が用意されています。

constresult1=newPromise(resolve=>resolve({foo:1}))constresult2=Promise.resolve({foo:1})constresult3=newPromise((resolve,reject)=>reject(newError('エラー')))constresult4=Promise.reject(newError('エラー'))console.log(result1)console.log(result2)console.log(result3)console.log(result4)>>>Promise{{foo:1}}Promise{{foo:1}}Promise{<rejected>Error:エラー...(省略)}Promise{<rejected>Error:エラー...(省略)}

then(), catch(), finally()

then()

then()はPromiseインスタンスの状態がfulfilledまたはrejectedになった時実行するコールバックを登録するメソッドです。

promise.then(value=>{// 成功時の処理},err=>{// 失敗時の処理})

onFulfilledの引数には解決済みのPromiseインスタンスの値が、onRejectedの引数には拒否理由が渡されます。then()の戻り値は、登録したコールバックの戻り値で解決される新しいPromiseインスタンスです。また、then()の実行は元のPromiseインスタンスには影響を及ぼしません。
元のPromiseインスタンスが何らかの理由で拒否された時、onRejectedを省略するとthen()の戻り値のPromiseインスタンスも同じ理由で拒否されます。

conststringPromise=Promise.resolve('{"foo": 1}');console.log(stringPromise);constnumberPromise=stringPromise.then(str=>str.length);constunrecoveredPromise=Promise.reject(newError('エラー')).then(()=>1)setTimeout(()=>{console.log(numberPromise);console.log(unrecoveredPromise)},1000)>>>Promise{'{"foo": 1}'}(node:9334)UnhandledPromiseRejectionWarning:Error:エラー...(省略)Promise{10}Promise{<rejected>Error:エラー...(省略)

一方、onRejectedを省略せずに何か値を返すようにするとその値で解決されたPromiseインスタンスが得られます。この結果UnhandledPromiseRejectionWarningが出力されなくなります。

constunrecoveredPromise=Promise.reject(newError('エラー')).then(()=>1,err=>err.message)setTimeout(()=>console.log(unrecoveredPromise),1000)>>>Promise{'エラー'}

then()によるPromiseのチェーンで非同期処理の逐次実行を容易に実装できます。

asyncFunc1(input).then(asyncFunc2).then(asyncFunc3).catch(err=>{// エラーハンドリング})

catch()

catch()も非同期処理の結果をハンドリングするためのインスタンスメソッドの1つです、then()の引数に渡すonFulfilled, onRejectedはどちらも省略可能で、then()の代わりにcatch()を使用できます。

constcatchedPromise=Promise.reject(newError('エラー')).catch(()=>0)setTimeout(()=>console.log(catchedPromise),1000)>>>Promise{0}

Promiseチェーンの最後にcatch()を記述すれば、エラーハンドリングを1箇所に集約できます。

asyncFunc1(input).then(asyncFunc2,err=>{// エラーハンドリング}).then(result=>{// 処理},err=>{// asyncFunc2用のエラーハンドリング})asyncFunc1(input).then(asyncFunc2).then(result=>{// 処理}).catch(err=>{// エラーハンドリングを集約できる})

finally()

try...catch構文におけるfinallyブロック相当の機能を提供している。すなわち、非同期処理が成功したかどうかに関わらず、Promiseインスタンスがsettled状態になった時実行されるコールバックを登録できます。

constonFinally=Promise.resolve().finally(()=>console.log('finallyのコールバック'));console.log(onFinally);>>>Promise{<pending>}finallyのコールバック

catch()と異なり、finally()のコールバックの戻り値はPromiseインスタンスが解決される値に影響しません。

constreturnValueInFinally=Promise.resolve(1).finally(()=>2)setTimeout(()=>console.log(returnValueInFinally),1000)>>>Promise{1}

異常系ではコールバックの挙動が返されるPromiseインスタンスに影響します。
コールバックないでエラーがthrowされる場合や、コールバックがrejectedなPromiseインスタンスを返す場合、finally()の返すPromiseインスタンスも同じ理由で拒否されます。

constthrowErrorInFinally=Promise.resolve(1).finally(()=>{thrownewError('エラー')});setTimeout(()=>console.log(throwErrorInFinally),1000)>>>(node:9673)UnhandledPromiseRejectionWarning:Error:エラー...(省略)Promise{<rejected>Error:エラー...(省略)

コールバックの戻り値がPromiseインスタンスの場合、finally()の返すPromiseインスタンスはコールバックの返すPromiseインスタンスが解決されるまで解決されません。

Promise.resolve('foo').finally(()=>newPromise(resolve=>setTimeout(()=>{console.log('finally() 1秒経過')resolve()},1000))).then(console.log)>>>finally()1秒経過foo

Promiseのスタティックメソッドを自用した並行実行

Promise.all()

Promise.all()の返すPromiseインスタンスは、引数に含まれるPromiseインスタンスが全てfulfilledになった時fulfilledになり、1つでもrejectedになるとその他のPromiseインスタンスの結果を待たずにrejectedになります。

// 正常系constallResolved=Promise.all([1,Promise.resolve('foo'),Promise.resolve(true)])setTimeout(()=>console.log(allResolved),1000)>>>Promise{[1,'foo',true]}
// 異常系constcontainRejected=Promise.all([1,Promise.reject('foo'),Promise.resolve(true)])setTimeout(()=>console.log(containRejected),1000)>>>(node:9936)UnhandledPromiseRejectionWarning:foo...(省略)Promise{<rejected>'foo'}

複数の非同期処理を逐次実行する必要がなければ、Promise.all()により並行実行する方が処理が早くなります。

importperf_hooksfrom'perf_hooks';constasyncFunc=()=>newPromise(resolve=>setTimeout(resolve,1000))conststart=perf_hooks.performance.now();asyncFunc().then(asyncFunc).then(asyncFunc).then(asyncFunc).then(()=>console.log('逐次実行所要時間',perf_hooks.performance.now()-start))Promise.all([asyncFunc(),asyncFunc(),asyncFunc(),asyncFunc()]).then(()=>console.log('並行実行所要時間',perf_hooks.performance.now()-start))>>>並行実行所要時間1002.2058520019054逐次実行所要時間4016.1091419905424

Promise.race()

Promise.race()の返すPromiseインスタンスは、引数に含まれるPromiseインスタンスが1つでもsettledになると、その他のPromiseインスタンスの結果を待たずにそのPromiseインスタンスと同じ状態になります。

constwait=(time)=>newPromise(resolve=>setTimeout(resolve,time))constfulfilledFirst=Promise.race([wait(10).then(()=>1),wait(30).then(()=>'foo'),wait(20).then(()=>Promise.reject(newError('エラー')))])constrejectFirst=Promise.race([wait(20).then(()=>1),wait(30).then(()=>'foo'),wait(10).then(()=>Promise.reject(newError('エラー')))])constcontainNonPromise=Promise.race([wait(10).then(()=>1),'foo',wait(20).then(()=>Promise.reject(newError('エラー')))])setTimeout(()=>console.log(fulfilledFirst),1000)setTimeout(()=>console.log(rejectFirst),1000)setTimeout(()=>console.log(containNonPromise),1000)>>>Promise{1}Promise{<rejected>Error:エラー...(省略)}Promise{'foo'}

引数にから配列を渡すと、解決されることのないPromiseインスタンスを返します。

constraceWithEmptyArray=Promise.race([]);setTimeout(()=>console.log(raceWithEmptyArray),1000);>>>Promise{<pending>}

Promise.allSettled()

Promise.allSettled()の返すPromiseインスタンスは、引数に含まれるPromiseインスタンスが全てsettledになったときfulfilledになります。

constallSettled=Promise.allSettled([1,Promise.resolve('foo'),Promise.reject(newError('エラー')),Promise.resolve(true)])setTimeout(()=>console.log(allSettled),1000>>>Promise{[{status:'fulfilled',value:1},{status:'fulfilled',value:'foo'},{status:'rejected',reason:Error:エラー},{status:'fulfilled',value:true}]}

OpenAPI (Swagger) 形式のyamlからrequestBody/responsesのサンプルJSONを出力する

$
0
0

はじめに

ユニットテストとかを書くためにAPIのリクエストとレスポンスがJSON形式で欲しいとなったときに、今まではOpenAPIドキュメントをReDocで表示してそこに載っているサンプルをコピー&JSON形式で保存し直すみたいなことをしていました。
ただしこの方法は、APIの数が増えてきてかつ変更もちょくちょくあるような場合だと結構面倒な作業ですし、CIにも組み込みにくいです。

スクリーンショット 2021-03-10 21.54.52.png

わざわざこの画面を経由しないでもOpenAPIドキュメントのyamlからこのrequestBody/responsesだけを直接JSONで吐き出す方法は何か無いものかと色々調べてみたのですが、元のOpenAPIドキュメントをyaml ⇆ JSONで変換する類の話しか見つからなかったので簡単なスクリプトを書くことにしました。
(もしかしたら自分の調査不足なだけで実は既にこの手のことをローカルでやってくれるツールはあるのかもしれません...)

JSON出力スクリプト

やりたいこととしては以下のような感じです

  • コマンドライン引数でOpenAPI (Swagger) 形式のyamlファイルを渡すとそのファイル内の全APIのrequestBody/responsesをJSON形式で出力する
  • GETなどrequestBodyが無い場合は空のJSONを出力する
  • responsesはステータスコード毎にJSONを出力する

理想はReDocのコピーボタンをクリックしたときにクリップボードに保存されるのと同じような形式のJSONを取得することだったので、当該処理がどうなっているのかソースコードをちょっと読んでみたところ、openapi-samplerというのを使ってそうだということが分かりました。
今回はそれを利用するためにスクリプトはNode.jsで書いています。

完成したスクリプトはこんな感じです。
package.jsonとか含めたコードはGitHubで公開しています。
https://github.com/NatsuToku/openapi-sample-json-generator

generator.js
// ファイル出力の設定constinputFile=process.argv.length==3?process.argv[2]:"openapi.yaml";constoutputBasePath="./output";constoutputRequestJSONName="request";constoutputResponseJSONPrefix="response";constJSONSpaceNum=4;// パース方法の設定constmediaType="application/json";constskipNonRequired=false;constskipReadOnly=true;constskipWriteOnly=false;// モジュールのimportconstSwaggerParser=require("@apidevtools/swagger-parser");constOpenAPISampler=require("openapi-sampler");constfs=require("fs");(async()=>{// $refポインタを含まないOpenAPI定義のオブジェクトを取得するconstparser=awaitSwaggerParser.dereference(inputFile);// APIのパス毎に処理するObject.keys(parser.paths).forEach(function(path){// 同じパスのメソッド毎に処理するObject.keys(parser.paths[path]).forEach(function(method){// ファイル出力用のパスをAPIのパスとメソッドに基づいて設定する// ex) パスが /a/b/c でメソッドが POST -> /outputBasePath/a_b_c/postconstoutputPath=`${outputBasePath}/${path.replace("/","").replace(/\//g,"_")}/${method}`;// outputBasePath内にファイル出力用のディレクトリを作成するfs.mkdir(outputPath,{recursive:true},(err)=>{constapi=parser.paths[path][method];// requestBodyが存在している場合はサンプルJSONオブジェクトを生成するletrequestSample={};if(api.hasOwnProperty("requestBody")&&api.requestBody.hasOwnProperty(mediaType)){requestSample=OpenAPISampler.sample(api.requestBody.content[mediaType].schema,{skipNonRequired:skipNonRequired,skipReadOnly:skipReadOnly,skipWriteOnly:skipWriteOnly,});}// requestBodyのJSONを出力する (requestBodyが存在しない場合は空)fs.writeFileSync(`${outputPath}/${outputRequestJSONName}.json`,JSON.stringify(requestSample,null,JSONSpaceNum));// ステータスコード毎に処理するconstresponses=api.responses;Object.keys(responses).forEach(function(status){// responseのcontentが存在している場合はサンプルJSONオブジェクトを生成するletresponseSample={};if(responses[status].hasOwnProperty("content")&&responses[status].content.hasOwnProperty(mediaType)){responseSample=OpenAPISampler.sample(responses[status].content[mediaType].schema,{skipNonRequired:skipNonRequired,skipReadOnly:skipReadOnly,skipWriteOnly:skipWriteOnly,});}// responsesのJSONを出力する (responseのcontentが存在しない場合は空)fs.writeFileSync(`${outputPath}/${outputResponseJSONPrefix}_${status}.json`,JSON.stringify(responseSample,null,JSONSpaceNum));});});});});})();

実行結果

以下のコマンドで実行します。

node ./generator.js <OPENAPI_FILE_NAME>

引数のファイル名は省略するとデフォルトでは「./openapi.yaml」を読むようにしています。
yaml形式でしかテストしてませんがswagger-parserが対応しているのでおそらくJSON形式でも大丈夫だと思われます。

例えば以下のようなyamlを読ませた場合(内容は適当です)

openapi.yaml
openapi:3.0.0info:title:"BookAPI"version:"1.0"servers:-url:"https://xxxxxx.com"paths:/book:get:summary:Get book listdescription:"本の一覧を取得する"responses:200:description:"本の一覧"content:application/json:schema:type:"array"items:$ref:"#/components/schemas/BookIndex"400:description:Bad Requestcontent:application/json:schema:$ref:"#/components/schemas/BadRequest"post:summary:Create bookdescription:"本の情報を新規登録する"requestBody:content:application/json:schema:$ref:"#/components/schemas/Book"responses:200:description:"OK"content:application/json:schema:$ref:"#/components/schemas/BookIndex"400:description:Bad Requestcontent:application/json:schema:$ref:"#/components/schemas/BadRequest"/book/{id}:get:summary:Get book detaildescription:"本の詳細を取得する"parameters:-name:idin:pathdescription:"uniquekey"required:trueschema:type:integerresponses:200:description:"本の一覧"content:application/json:schema:$ref:"#/components/schemas/BookIndex"400:description:Bad Requestcontent:application/json:schema:$ref:"#/components/schemas/BadRequest"delete:summary:Delete bookdescription:"本の登録を削除する"parameters:-name:idin:pathdescription:"uniquekey"required:trueschema:type:integerresponses:200:description:OK400:description:Bad Requestcontent:application/json:schema:$ref:"#/components/schemas/BadRequest"components:schemas:Author:type:objectrequired:-nameproperties:name:type:stringexample:"test"age:type:numberexample:30gender:type:stringexample:"unknown"Book:type:objectrequired:-title-price-authorsproperties:title:type:stringexample:"booktitle"price:type:numberexample:1500authors:type:arrayitems:$ref:"#/components/schemas/Author"category:type:"string"description:"bookcategory"example:"horror"BookIndex:allOf:-type:objectproperties:id:type:integerdescription:"uniquekey"-$ref:"#/components/schemas/Book"BadRequest:type:objectrequired:-messageproperties:message:type:stringexample:"error"

こんな感じに出力されます

output
├──book
│   ├── get
│   │    ├── request.json
│   │    ├── response_200.json
│   │    └── response_400.json
│   └─── post
│        ├── request.json
│        ├── response_200.json
│        └── response_400.json
└──book_{id}├── delete
    │    ├── request.json
    │    ├── response_200.json
    │    └── response_400.json
    └─── get
         ├── request.json
         ├── response_200.json
         └── response_400.json

例えば/bookのPOSTのresponse_200.jsonの中身はこうなってます

response_200.json
{"id":0,"title":"book title","price":1500,"authors":[{"name":"test","age":30,"gender":"unknown"}],"category":"horror"}

スクリーンショット 2021-03-10 22.09.57.png

コード内容の説明

generator.js
// ファイル出力の設定constinputFile=process.argv.length==3?process.argv[2]:"openapi.yaml";constoutputBasePath="./output";constoutputRequestJSONName="request";constoutputResponseJSONPrefix="response";constJSONSpaceNum=4;// パース方法の設定constmediaType="application/json";constskipNonRequired=false;constskipReadOnly=true;constskipWriteOnly=false;

以下の内容を設定しています。

  • inputFile: OpenAPI (Swagger) 形式のファイル。コマンドライン引数で指定。なければデフォルト「openaip.yaml」
  • outputBasePath:JSONファイルを出力するディレクトリ
  • outputRequestJSONName:requestBodyのJSONを出力するときのファイル名。この場合だと「request.json」
  • outputResponseJSONPrefix:responsesのcontentのJSONを出力するときのファイル名のプレフィックス。この場合だと「response_<status_code>.json」
  • JSONSpaceNum:出力するJSONファイルのインデントのスペース数
  • mediaType:OpenAPIをパースするときに対象とするメディアタイプ。"application/json"固定で良さそう
  • skipNonRequiredrequiredがtrueのパラメータをスキップする(JSONで出力しない)かどうか
  • skipReadOnly:readOnlyがtrueのパラメータをスキップする(JSONで出力しない)かどうか
  • skipWriteOnlywriteOnlyがtrueのパラメータをスキップする(JSONで出力しない)かどうか

generator.js
// モジュールのimportconstSwaggerParser=require("@apidevtools/swagger-parser");constOpenAPISampler=require("openapi-sampler");constfs=require("fs");
  • @apidevtools/swagger-parser
    • OpenAPI (Swagger) 形式のファイルを渡すとrefの解決をした状態のオブジェクトを返してくれる
    • 素でyamlファイルを開いてしまうとrefもそのままになってしまうので使用
  • openapi-sampler
    • OpenAPI Schemaオブジェクトを渡すとサンプルのJSONオブジェクトを返してくれる
    • ReDocでは内部的にこれを利用している模様
  • fs
    • ファイル関係の操作で使用

generator.js
(async()=>{// $refポインタを含まないOpenAPI定義のオブジェクトを取得するconstparser=awaitSwaggerParser.dereference(inputFile);

swagger-parserがawaitな関数なのでasync functionで囲んだ上で入力ファイルから $refポインタを含まないOpenAPI定義のオブジェクトを取得しています。


generator.js
// APIのパス毎に処理するObject.keys(parser.paths).forEach(function(path){// 同じパスのメソッド毎に処理するObject.keys(parser.paths[path]).forEach(function(method){

OpenAPIのpathsの中の各メソッド毎に処理します。
このforEach内でさらにforEatchを回す書き方はなんとなくもっと良い書き方がある気がしています...


generator.js
// ファイル出力用のパスをAPIのパスとメソッドに基づいて設定する// ex) パスが /a/b/c でメソッドが POST -> /outputBasePath/a_b_c/postconstoutputPath=`${outputBasePath}/${path.replace("/","").replace(/\//g,"_")}/${method}`;

JSONファイルを出力するパスは、OpenAPIのpathsとmethodで決めています。
pathはそのままだと「/a/b/c」みたいな形式なので、最初の「/」は消して、残りの「/」は全て「_」に置換することで「 a_b_c」みたいなディレクトリになり、さらにその下にgetやpostと言ったディレクトリを作ります。


generator.js
// outputBasePath内にファイル出力用のディレクトリを作成するfs.mkdir(outputPath,{recursive:true},(err)=>{constapi=parser.paths[path][method];

先ほど定義したoutputPathのディレクトリを作成します。
recursiveをtrueにすることで再帰的に作成。
既にディレクトリがあるとerrになるのですが、ここではerrが発生してもそのまま握り潰してます。
(のでerr変数はこの後使われない)


generator.js
// requestBodyが存在している場合はサンプルJSONオブジェクトを生成するletrequestSample={};if(api.hasOwnProperty("requestBody")&&api.requestBody.hasOwnProperty(mediaType)){requestSample=OpenAPISampler.sample(api.requestBody.content[mediaType].schema,{skipNonRequired:skipNonRequired,skipReadOnly:skipReadOnly,skipWriteOnly:skipWriteOnly,});}// requestBodyのJSONを出力する (requestBodyが存在しない場合は空)fs.writeFileSync(`${outputPath}/${outputResponseJSONPrefix}_${status}.json`,JSON.stringify(requestSample,null,JSONSpaceNum));

apiにrequestBodyが含まれかつmediaTypeがapplication/jsonだったらopenapi-samplerでサンプルのオブジェクトを作成し、その後そのオブジェクトをJSONファイルとして出力します。
GETのようにrequestBodyが無いような場合は空のJSONを出力します。
(出力したくなければwriteFileSyncの前に条件判断を入れれば良さそう)


generator.js
// ステータスコード毎に処理するconstresponses=api.responses;Object.keys(responses).forEach(function(status){// responseのcontentが存在している場合はサンプルJSONオブジェクトを生成するletresponseSample={};if(responses[status].hasOwnProperty("content")&&responses[status].content.hasOwnProperty(mediaType)){responseSample=OpenAPISampler.sample(responses[status].content[mediaType].schema,{skipNonRequired:skipNonRequired,skipReadOnly:skipReadOnly,skipWriteOnly:skipWriteOnly,});}// responsesのJSONを出力する (responseのcontentが存在しない場合は空)fs.writeFileSync(`${outputPath}/response_${status}.json`,JSON.stringify(responseSample,null,JSONSpaceNum)

requestBodyと同様にcontentが含まれかつmediaTypeがapplication/jsonだったらopenapi-samplerでサンプルのオブジェクトを作成し、その後そのオブジェクトをJSONファイルとして出力します。
requestBodyとの違いとしてはresponsesはステータスコード毎に複数設定可能であるため、処理もステータスコードをforEatchで回しています。
また、出力するファイル名のサフィックスにステータスコードをくっつけるようにしています。

最後に

普段Node.jsはあまり書いてない+こういうスクリプト的なものは初めて書いたのでコマンドライン引数の取り方とかファイルの保存方法とか個人的には勉強になりました。
Node.jsの書き方のベストプラクティス的なことを把握しないでフィーリングで書いている部分が多いので、ここの書き方微妙だよ的な指摘があればバンバンしていただけるとありがたいです。

参考

OpenAPI Specification - Version 3.0.3 | Swagger
GitHub - Redocly/openapi-sampler: Tool for generation samples based on OpenAPI(fka Swagger) payload/response schema
Swagger 2.0 and OpenAPI 3.0 parser/validator | Swagger Parser
Node.jsでコマンドライン引数を取得する - Qiita
[Node.js]ディレクトリの作成と削除をする
How can I pretty-print JSON using node.js? - Stack Overflow


【コピペOK】bashでnodeとyarnのインストールをする方法

$
0
0

こんにちは、くりぱんです。

この記事で実現できること

  • homebrewのインストール
  • nodebrewのインストール
  • Node.jsのインストール
  • yarnのインストール

開発環境

  • macOS Catalina
  • bash

説明

フロントエンドの環境を構築している際にnode.jsとyarnが必要になったので、nodebrewを使って、node.jsとyarnをインストールしていきます。
※なお、今回はよく使われているbashで実装していきます。

実装の流れ

  • Homebrewのインストール
  • Nodebrewのインストール
  • nodebrewのPATHを通す
  • Node.jsのインストール
  • yarnのインストール

実装

Homebrewのインストール

Homebrewはパッケージ管理システムの一つで、様々なソフトウェアの導入を簡単にしてくれるツールです。
今回はこちらを使用していくので、インストールしていない人は下記のコマンドを実行して、インストールしてください。インストールしている人は飛ばしてオッケーです。なお、インストールしているかわからない方は$ brew --versionと叩いて、エラーが出た場合はインストールされていないので、下記のコマンドを叩いてください。

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

公式サイト:https://brew.sh/index_ja

nodebrewのインストール

NodebrewはNode.jsのバージョンを管理するバージョン管理ツールです。今回はこのnodebrewを使用して、Node.jsとyarnを一括管理できるようにしていきます。
下記のコマンドを実行して、先ほどインストールしたHomebrewを使用してnodebrewをインストールしてください。すでにインストールしてる人はHomebrew同様飛ばしてオッケーです。インストールしてるかわからない方は$ nodebrewと叩いてください。

$ brew install nodebrew

これでnodebrewのインストールは終わりです。

nodebrewのPATHを通す

Nodebrewのコマンドを利用するためにnodebrewにPATHを通していきます。
下記のコマンドを実行してください。

$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile

追加したパスを適用していきます。

$ source ~/.bash_profile

これでNode.jsとyarnをインストールする準備が完了しました。

Node.jsのインストール

それでは、nodebrewを使用して、Node.jsをインストールしていきます。

$ nodebrew install-binary latest

インストールが完了したら、インストールされたNode.jsのバージョンを確認して、そのバージョンを使う設定にしていきます。

$ nodebrew list

v15.11.0などのバージョンが表示されていればOKです。このバージョンは人によって違うので、数字が違くても気にしなくて大丈夫です。

バージョンを確認したら、下記コマンドのバージョンを自分がインストールしたバージョンにしてコマンドを実行してください。

$ nodebrew use v15.11.0

下記コマンドでNode.jsのバージョンを確認してください。

$ node -v
v15.11.0

yarnのインストール

最後にyarnのインストールをして終わりです。

$ npm install -g yarn

バージョン確認もしておきましょう

$ yarn -v
1.22.10

最後に

以上でbashを使用したNode.jsとyarnのインストールは終了です。
各々フロントの開発を楽しんじゃってください!

少しでも役に立った!という時は、LGTMをポチッと、、、笑
1つでもLGTMが付くとその日がハッピーになるんです!
役に立たなかった時は、怒らないでコメント頂けると幸いです笑

Twitterもやってます!
プログラミングや金融知識についてやエンジニアの現実についてつぶやいています!
よかったら見てみてくださいね!

Laravelでnpm installしたら、found 1 high severity vulnerabilityと怒られた話

$
0
0

こんにちは、くりぱんです。

この記事で実現できること

開発環境

  • OS:Windows
  • Laravel:6.19
  • npm:6.14.8
  • node.js:v11.13.0

説明

$ npm installしたらこんなエラーが、、、

45 packages are looking for funding
  run `npm fund` for details

found 1 high severity vulnerability
  run `npm audit fix` to fix them, or `npm audit` for details

重大な貧弱性が見つかったとのこと、、、
これはだめだと思ってなんとかこのエラーを解決してみましたので、同じエラーが出ている方は参考にしてください。

実装

とりあえず、上のエラー文を見てみると$ npm auditを実行してといわれているので、実行。

                       === npm audit security report ===

# Run  npm install --save-dev resolve-url-loader@3.1.2  to resolve 1 vulnera
SEMVER WARNING: Recommended action is a potentially breaking change

  High            Prototype Pollution

  Package         object-path <これが重大な貧弱性あるやつ>

  Dependency of   resolve-url-loader [dev]

  Path            resolve-url-loader > adjust-sourcemap-loader > object-path

  More info       https://npmjs.com/advisories/1573

found 1 high severity vulnerability in 1088 scanned packages
  1 vulnerability requires semver-major dependency updates.

よく見るとobject-pathというパッケージにてPrototype Pollutionという貧弱性があるよと教えてくれてます。

さらによく見てみると、npm install --save-dev resolve-url-loader@3.1.2すれば解消できるよ!と言われているので、実行してみる。

$ npm install--save-dev resolve-url-loader@3.1.2

すると、下記のようになります。

  + resolve-url-loader@3.1.2
added 17 packages from 46 contributors, removed 17 packages, updated 2 packaited 1088 packages in 16.007s

45 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

おお~見事に貧弱性治りました。
これでエラーも何も出ないので大丈夫そうです。

npmでfound 〇 high severity vulnerabilityと怒られたら、まずはnpm auditをして、auditに言われた通りに実行してみましょう!

最後に

少しでも役に立った!という時は、LGTMをポチッと、、、笑
1つでもLGTMが付くとその日がハッピーになるんです!
役に立たなかった時は、怒らないでコメント頂けると幸いです笑

Twitterもやってます!
プログラミングや金融知識についてやエンジニアの現実についてつぶやいています!
よかったら見てみてくださいね!

$ npm run devを実行したらエラーが出た

$
0
0

目的

  • $ npm run devを実行したところエラーが出たので解決策をメモ的に残す。

エラーまでの経緯

  1. 下記コマンドを実行した。

    $npm run dev
    

エラー

  • 下記のエラーが出力された。

    [16:08:46]MacBook-Pro~/workspase/work/todo_app/todos$ npm run dev
    
    > @ dev /Users/okawashun/workspase/work/todo_app/todos
    > npm run development
    
    > @ development /Users/okawashun/workspase/work/todo_app/todos
    > mix
    
    Error: You are using an unsupported version of Node. Please update to at least Node v12.14
        at assertSupportedNodeVersion (/Users/okawashun/workspase/work/todo_app/todos/node_modules/laravel-mix/src/Engine.js:6:15)
        at executeScript (/Users/okawashun/workspase/work/todo_app/todos/node_modules/laravel-mix/bin/cli.js:61:5)
        at Command.program.command.description.option.action.cmd (/Users/okawashun/workspase/work/todo_app/todos/node_modules/laravel-mix/bin/cli.js:47:13)
        at Command.listener [as _actionHandler] (/Users/okawashun/workspase/work/todo_app/todos/node_modules/commander/index.js:426:31)
        at Command._parseCommand (/Users/okawashun/workspase/work/todo_app/todos/node_modules/commander/index.js:1002:14)
        at Command._dispatchSubcommand (/Users/okawashun/workspase/work/todo_app/todos/node_modules/commander/index.js:953:18)
        at Command._parseCommand (/Users/okawashun/workspase/work/todo_app/todos/node_modules/commander/index.js:979:12)
        at Command.parse (/Users/okawashun/workspase/work/todo_app/todos/node_modules/commander/index.js:801:10)
        at Command.parseAsync (/Users/okawashun/workspase/work/todo_app/todos/node_modules/commander/index.js:828:10)
        at run (/Users/okawashun/workspase/work/todo_app/todos/node_modules/laravel-mix/bin/cli.js:50:19)
    npm ERR! code ELIFECYCLE
    npm ERR! errno 1
    npm ERR! @ development: `mix`
    npm ERR! Exit status 1
    npm ERR! 
    npm ERR! Failed at the @ development script.
    npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
    
    npm ERR! A complete log of this run can be found in:
    npm ERR!     /Users/okawashun/.npm/_logs/2021-03-10T07_08_54_349Z-debug.log
    npm ERR! code ELIFECYCLE
    npm ERR! errno 1
    npm ERR! @ dev: `npm run development`
    npm ERR! Exit status 1
    npm ERR! 
    npm ERR! Failed at the @ dev script.
    npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
    
    npm ERR! A complete log of this run can be found in:
    npm ERR!     /Users/okawashun/.npm/_logs/2021-03-10T07_08_54_372Z-debug.log
    

解決までの経緯

  1. 「Please update to at least Node v12.14」と出力されている。現在のNode.jsのバージョンをまず確認してみる。下記コマンドを実行してNode.jsのバージョンを確認した。

    $node --version
  2. 下記のように出力された。確かにかなり古い。

    # use a specific version number
    node use v8.9.4
    
  3. 下記コマンドを実行してNode.jsの12.14をインストールする。

    $nodebrew install v12.14
    
  4. インストール完了後に下記コマンドを実行してNode.jsのバージョンを切り替える。

    $nodebrew use v12.14.1
    
  5. 下記コマンドを実行してNode.jsのバージョンを確認する。

    $node --version
  6. Node.jsのバージョンがv12.14.1になったことが確認できた。

  7. 再度$ npm run devを実行したところ正常に実行された。

Expressでのエラーハンドリングでハマった

$
0
0

Express触ってたら例外処理でハマったのでメモを残しておきます。そんなに大した理由ではありませんでした。

先に結論

ドキュメントをちゃんと読む

前提

Node.js 14.15.3
Express 4.17.1
Mongoose 5.11.9
express-async-errors 3.1.1

起こったこと

所謂MERN stackを学習中なのですが、ユーザーを作成してDBに保存するAPIを作成していました。こんな感じのよくチュートリアルにあるようなやつです

./controllers/users.js
constusersRouter=require('express').Router()constUser=require('../models/user')constbcrypt=require('bcrypt')usersRouter.get('/',async(request,response)=>{constusers=awaitUser.find({})response.json(users)})/**
 * create new user
 */usersRouter.post('/',async(request,response)=>{constbody=request.bodyconstsaltRounds=10constpasswordHash=awaitbcrypt.hash(body.password,saltRounds)constuser=newUser({username:body.username,name:body.name,password:passwordHash})constsavedUser=awaituser.save()response.json(savedUser)})module.exports=usersRouter

で、usernameのユニークバリデーションを確認するためにちょっとPostmanでPOSTリクエスト送ってみたのですが、全然レスポンスが返ってきませんでした。
確認すると以下のようなエラーが。

(node:32647) UnhandledPromiseRejectionWarning: ValidationError: User validation failed: username: Error, expected `username` to be unique. Value: `ichiro`
    at model.Document.invalidate (/Users/xxx/projects/bloglist/node_modules/mongoose/lib/document.js:2693:32)
    at /Users/xxx/projects/bloglist/node_modules/mongoose/lib/document.js:2513:17
    at /Users/xxx/projects/bloglist/node_modules/mongoose/lib/schematype.js:1241:9
    at processTicksAndRejections (internal/process/task_queues.js:75:11)
(node:32647) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:32647) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

try catchしろよという話なのですが問題はそこではなくて、express-async-errorsをインストールしていたのにこのエラーが出たという点です。

express-async-errorはExpressでasync/awaitを使用する際にtry catchを書かなくてもよしなにやってくれるようなライブラリです。
https://www.npmjs.com/package/express-async-errors

意図していた挙動

以下のようなエラーハンドリング用のミドルウェアを定義していたので、バリデーションエラーの場合はステータスコード400でエラーメッセージが返ってくる想定でした。

./utils/middleware.js
consterrorHandler=(error,request,response,next)=>{if(error.name==='ValidationError'){returnresponse.status(400).json({error:error.message})}next(error)}module.exports={errorHandler}

で、上記のmiddlewareはapp.jsというファイルでuseしているので適用されるはずです。

app.js
constexpress=require('express')constapp=express()constmongoose=require('mongoose')constcors=require('cors')constblogsRouter=require('./controllers/blogs')constusersRouter=require('./controllers/users')require('express-async-errors')//<- express-async-errorsはここでrequireconstconfig=require('./utils/config')constmiddleware=require('./utils/middleware')mongoose.connect(config.MONGODB_URL,{useNewUrlParser:true,useUnifiedTopology:true,useFindAndModify:false,useCreateIndex:true})app.use(cors())app.use(express.json())app.use('/api/blogs',blogsRouter)app.use('/api/users',usersRouter)app.use(middleware.errorHandler)//<-これmodule.exports=app

ちなみにエントリーポイントのindex.jsはこういう感じです。ここは特に問題なさそう

index.js
consthttp=require('http')constapp=require('./app')constconfig=require('./utils/config')constserver=http.createServer(app)server.listen(config.PORT,()=>{console.log(`Server running on port ${config.PORT}`)})

app.jsがどうも怪しそうなのですが、ミドルウェアのuseの順番もとくに問題なさそうですし、Expressのドキュメントを色々確認してみてもいまいち原因が掴めないまま時間が過ぎました。
で、そもそもexpress-async-errorsの使い方が何か間違っているのではと思い始め、express-async-errorsのドキュメントを確認しました。すると

スクリーンショット 2021-03-11 18.59.45.png

Then require this script somewhere before you start using it

修正

app.js
constexpress=require('express')require('express-async-errors')constapp=express()constmongoose=require('mongoose')constcors=require('cors')constblogsRouter=require('./controllers/blogs')constusersRouter=require('./controllers/users')constconfig=require('./utils/config')constmiddleware=require('./utils/middleware')mongoose.connect(config.MONGODB_URL,{useNewUrlParser:true,useUnifiedTopology:true,useFindAndModify:false,useCreateIndex:true})app.use(cors())app.use(express.json())app.use('/api/blogs',blogsRouter)app.use('/api/users',usersRouter)app.use(middleware.errorHandler)module.exports=app

ドキュメントをちゃんと読んでない&そもそもNode.jsのrequireの仕組みについて理解が浅いというのが招いたミスでした。

[Node.js] 非同期処理 - ジェネレータ編

$
0
0

ジェネレータ

ジェネレータ関数は、処理の途中で停止したり再開したりできる仕組みを持った特殊な関数です。

ジェネレータの生成

function*generatorFunc(){yield1yield2yield3}constgenerator=generatorFunc()

ジェネレータ関数には2つの明確な特徴があります。1つは、functionの後ろに*がつくことで、もう1つはyieldキーワードです。

function*generatorFunc(){console.log('ジェネレータ関数開始')console.log('yield 1')yield1console.log('yield 2')yield2console.log('yield 3')yield3console.log('ジェネレータ関数終了')return'ジェネレータ関数戻り値'}constgenerator=generatorFunc()console.log(generator.next())console.log(generator.next())console.log(generator.next())console.log(generator.next())console.log(generator.next())console.log(generator.next())>>>ジェネレータ関数開始yield1{value:1,done:false}yield2{value:2,done:false}yield3{value:3,done:false}ジェネレータ関数終了{value:'ジェネレータ関数戻り値',done:true}{value:undefined,done:true}{value:undefined,done:true}

ジェネレータ関数を実行するとジェネレータを返されますが、この時点ではジェネレータ関数の中の処理は実行されません。生成されたジェネレータのnext()を実行して初めて中の処理が実行されます。実行した後も全て実行されるのではなく、最初のyieldまで実行されて、そこで一時停止します。もう一度実行すると処理が再開されて2つ目のyieldまで実行します。
ジェネレータ関数の処理が完了(上記だと4回目のnext())すると{ value: '値', done: true }(上記だと{ value: 'ジェネレータ関数戻り値', done: true })となります。処理終了後も実行すると{ value: undefined, done: true }が返ってきます。

イテレータとイテラブル

ジェネレータのnext()メソッドの挙動は、イテレータプロトコルという使用に準拠しています。イテレータプロトコルは値の配列を生成するための標準的な方法を定義したもので、この仕様ではnext()メソッドがvalue, doneという2つのプロパティを含むオブジェクトを返します。
ジェネレータは、イテラブルプロトコルという仕様を満たしたイテラブルです。

イテラブルは反復可能なオブジェクトのため、for...of構文で利用可能です。

constgenerator2=generatorFunc()for(constvofgenerator2){console.log(v)}>>>ジェネレータ関数開始yield11yield22yield33ジェネレータ関数終了

next()/throw()

- next()に引数を渡して実行

function*resetableGeneratorFunc(){letcount=0while(true){if(yieldcount++){count=0}}}constresetableGenerator=resetableGeneratorFunc()console.log(resetableGenerator.next())console.log(resetableGenerator.next())console.log(resetableGenerator.next())console.log(resetableGenerator.next(true))console.log(resetableGenerator.next())console.log(resetableGenerator.next())>>>{value:0,done:false}{value:1,done:false}{value:2,done:false}{value:0,done:false}{value:1,done:false}{value:2,done:false}

resetableGeneratorFunc()は値をカウントアップする関数ですが、yieldの結果が真に評価された場合、カウンタの値を0にリセットします。結果として、生成されたジェネレータのnext()を真に評価される引数で実行すると、0から値を返します。

  • throw()の使用
function*tryCatchGeneratoreFunc(){try{yield1}catch(err){console.log('エラーをキャッチ',err)yield2}}consttryCatchGeneratore=tryCatchGeneratoreFunc()console.log(tryCatchGeneratore.next())console.log(tryCatchGeneratore.throw(newError('エラー')))console.log(tryCatchGeneratore.next())>>>{value:1,done:false}エラーをキャッチError:エラー...(省略){value:2,done:false}{value:undefined,done:true}

エラーがジェネレータ関数ないでキャッチされなかった場合、ジェネレータは終了し、throw()自体もエラーを投げます。

try{generatorFunc.throw(newError('エラー'))}catch(err){console.log('ジェネレータ外でエラーをキャッチ',err)}>>>ジェネレータ外でエラーをキャッチTypeError:generatorFunc.throwisnotafunction...(省略)

非同期プログラミング

function*asyncWithGeneratorFunc(json){try{constresult=yieldparseJSONAsync(json)console.log('パース結果',result)}catch(err){console.log('エラーをキャッチ',err)}}// 正常系constasyncWithGenerator1=asyncWithGeneratorFunc('{"foo": 1}')constpromise1=asyncWithGenerator1.next().valuepromise1.then(result=>asyncWithGenerator1.next(result))// 異常系constasyncWithGenerator2=asyncWithGeneratorFunc('不正なJSON')constpromise2=asyncWithGenerator2.next().valuepromise2.catch(err=>asyncWithGenerator2.throw(err))>>>パース結果{foo:1}エラーをキャッチSyntaxError:UnexpectedtokeninJSONatposition0atJSON.parse(<anonymous>)...(省略)

異常系の場合にエラーハンドリングを上記にように毎回書くのが手間なので以下のように書き直してみます。

functionhandleAsyncWithGenerator(generator,resolved){console.log(generator)const{done,value}=generator.next(resolved);if(done)returnPromise.resolve(value)returnvalue.then(resolved=>handleAsyncWithGenerator(generator,resolved),err=>generator.throw(err));}handleAsyncWithGenerator(asyncWithGeneratorFunc('{"foo": 1}'))handleAsyncWithGenerator(asyncWithGeneratorFunc('不正なJSON'))>>>パース結果{foo:1}エラーをキャッチSyntaxError:UnexpectedtokeninJSONatposition0atJSON.parse(<anonymous>)...(省略)
Viewing all 8820 articles
Browse latest View live