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

Node8系→12系にあげたらいっぱいエラー出た

$
0
0

概要

お世話になったnode8系が2019/12に寿命を迎えます:pray:

スクリーンショット 2019-11-19 15.19.44.png

https://github.com/nodejs/Release#release-schedule

そのため、node (v8.7.0) → (v12.13.0)に変更しましたが、いろいろとエラーが出たためそのメモ書きです。
(使っているmoduleによって個人差が出るため完全に自分のための覚書です:writing_hand:)

アップグレード方法

$ nodenv install 12.13.0

$ nodenv local 12.13.0

エラー発生

数個エラーが出たのですが、 node-sassを例にとります!

$ yarn 

:
:
Error: Missing binding /path/to/project/node_modules/node-sass/vendor/darwin-x64-64/binding.node
Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 10.x

Found bindings for the following environments:
  - OS X 64-bit with Node.js 8.x

This usually happens because your environment has changed since running `npm install`.
Run `npm rebuild node-sass` to download the binding for your current environment.
    at module.exports (/path/to/project/node_modules/node-sass/lib/binding.js:15:13)
    at Object.<anonymous> (/path/to/project/node_modules/node-sass/lib/index.js:14:35)
    at Module._compile (internal/modules/cjs/loader.js:778:30)

そのほかにもこんなエラーも出ました
node-pre-gyp WARN Using request for node-pre-gyp https download
node-pre-gyp WARN Tried to download(404): https://node-precompiled-binaries.grpc.io/grpc/v1.20.0/node-v72-linux-x64-glibc.tar.gz
node-pre-gyp WARN Pre-built binaries not found for grpc@1.20.0 and node@12.4.0 (node-v72 ABI, glibc) (falling back to source compile with node-gyp)
gyp ERR! build error
gyp ERR! stack Error: not found: make
gyp ERR! stack at getNotFoundError (/usr/lib/node_modules/npm/node_modules/which/which.js:13:12)
gyp ERR! stack at F (/usr/lib/node_modules/npm/node_modules/which/which.js:68:19)
gyp ERR! stack at E (/usr/lib/node_modules/npm/node_modules/which/which.js:80:29)
gyp ERR! stack at /usr/lib/node_modules/npm/node_modules/which/which.js:89:16
gyp ERR! stack at /usr/lib/node_modules/npm/node_modules/isexe/index.js:42:5
gyp ERR! stack at /usr/lib/node_modules/npm/node_modules/isexe/mode.js:8:5
gyp ERR! stack at FSReqCallback.oncomplete (fs.js:165:21)
gyp ERR! System Linux 4.18.0-22-generic
gyp ERR! command "/usr/bin/node" "/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "build" "--fallback-to-build" "--library=static_library" "--module=/home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc/grpc_node.node" "--module_name=grpc_node" "--module_path=/home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc" "--napi_version=4" "--node_abi_napi=napi" "--napi_build_version=0" "--node_napi_label=node-v72"
gyp ERR! cwd /home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc
gyp ERR! node -v v12.4.0
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok
node-pre-gyp ERR! build error
node-pre-gyp ERR! stack Error: Failed to execute '/usr/bin/node /usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js build --fallback-to-build --library=static_library --module=/home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc/grpc_node.node --module_name=grpc_node --module_path=/home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc --napi_version=4 --node_abi_napi=napi --napi_build_version=0 --node_napi_label=node-v72' (1)
node-pre-gyp ERR! stack at ChildProcess. (/home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc/node_modules/node-pre-gyp/lib/util/compile.js:83:29)
node-pre-gyp ERR! stack at ChildProcess.emit (events.js:200:13)
node-pre-gyp ERR! stack at maybeClose (internal/child_process.js:1021:16)
node-pre-gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:283:5)
node-pre-gyp ERR! System Linux 4.18.0-22-generic
node-pre-gyp ERR! command "/usr/bin/node" "/home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc/node_modules/.bin/node-pre-gyp" "install" "--fallback-to-build" "--library=static_library"
node-pre-gyp ERR! cwd /home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc
node-pre-gyp ERR! node -v v12.4.0
node-pre-gyp ERR! node-pre-gyp -v v0.12.0
node-pre-gyp ERR! not ok
Failed to execute '/usr/bin/node /usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js build --fallback-to-build --library=static_library --module=/home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc/grpc_node.node --module_name=grpc_node --module_path=/home/ahmed/Documents/alghanim-phase2-frontend/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc --napi_version=4 --node_abi_napi=napi --napi_build_version=0 --node_napi_label=node-v72' (1)
npm WARN @ng-bootstrap/ng-bootstrap@4.2.1 requires a peer of rxjs@^6.3.0 but none is installed. You must install peer dependencies yourself.
npm WARN @ngtools/webpack@6.0.8 requires a peer of typescript@~2.4.0 || ~2.5.0 || ~2.6.0 || ~2.7.0 but none is installed. You must install peer dependencies yourself.
npm WARN angular-in-memory-web-api@0.6.1 requires a peer of @angular/common@^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN angular-in-memory-web-api@0.6.1 requires a peer of @angular/core@^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN angular-in-memory-web-api@0.6.1 requires a peer of @angular/http@^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN angular2-csv@0.2.9 requires a peer of @angular/common@^6.0.0-rc.0 || ^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN angular2-csv@0.2.9 requires a peer of @angular/core@^6.0.0-rc.0 || ^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN The package @angular/compiler is included as both a dev and production dependency.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/webpack-dev-server/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/watchpack/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/rijs.resdir/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/karma/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/@schematics/update/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/@schematics/angular/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/@angular/compiler-cli/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/@angular/cli/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/@angular-devkit/schematics/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/@angular-devkit/core/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! grpc@1.20.0 install: node-pre-gyp install --fallback-to-build --library=static_library
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the grpc@1.20.0 install script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

モジュールがNodeのバージョンに対応していないときこういったエラーが出るようです

エラー対処

 $ yarn add node-sass@4.12.0

これでpackage.jsonが以下のように更新されます

{"name":"project","dependencies":{→ "node-sass":"4.12.0",}}

これで無事エラーがなくなる、と思ったら私の場合上記と同じエラーが出てしまいました…

エラー原因特定

  • yarn why で依存関係確認
 $ yarn why node-sass

[1/4] 🤔  Why do we have the module "node-sass"...?
[2/4] 🚚  Initialising dependency graph...
[3/4] 🔍  Finding dependency...
[4/4] 🚡  Calculating file sizes...

=> Found "node-sass@4.12.0"
info Has been hoisted to "node-sass"
info This module exists because it's specified in "dependencies".
info Disk size without dependencies: "6.03MB"
info Disk size with unique dependencies: "16.02MB"
info Disk size with transitive dependencies: "26.99MB"
info Number of shared dependencies: 113

=> Found "gulp-sass#node-sass@4.9.0"
info This module exists because "gulp-sass" depends on it.
info Disk size without dependencies: "6.06MB"
info Disk size with unique dependencies: "16.05MB"
info Disk size with transitive dependencies: "27.03MB"
info Number of shared dependencies: 113
✨  Done in 1.25s.

なんとgulp-sassと依存関係にあり、そちらは更新されていないことがわかりました。
ですのでpackage.jsonに以下を追加
(私の場合node-sass意外にも他2つ依存関係にあるモジュールがありました)

{"name":"project","dependencies":{"node-sass":"4.12.0",},"resolutions":{"**/**/fsevents":"^1.2.9","node-sass":"^4.12.0","grpc":"^1.20.2"}}

package.jsonresolutionsフィールドを書くことで、
特定のモジュールに依存しているモジュールのバージョンも指定できます

解決

無事解決しました

$ yarn
yarn install v1.17.3
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in ~~ s

Nodeの知識全然足りないので勉強しなきゃなーと考えさせられました:skull:

参考サイト

GitHub Issues

resolutionsについて


passportモジュールでfacebook認証

$
0
0

passportとは

passportはNode.jsで利用できる認証ミドルウェア(モジュール)です。
passportを利用することで、アプリに簡単にOAuth認証を組み込むことができます。

OAuth認証に関して、こちらの記事がすごく分かりやすいので読んでみてください。
一番分かりやすい OAuth の説明

passportの凄いところは、FacebookやTwitter、Googleなど、多くのアカウント認証を利用できる点です。

今回は、ExpressというNode.jsのフレームワークがグローバルインストールされている前提で進んでいきます。
参考:Expressフレームワークのインストールと簡単な使い方

passportを利用して、Facebook認証をしてみる

Facebook Developersでアプリを作成

まずは、Facebook認証を利用するために、https://developers.facebook.com/からアプリケーションの作成を行なってください。
スクリーンショット 2019-12-02 17.52.37.png
作成ができたら、アプリの設定ページで以下のように、アプリIDとapp secretが作成されています。
スクリーンショット 2019-12-02 17.53.49.png
このアプリIDとapp secretは、後で利用することになります。

passportで使うモジュールのインストール

続いて、passportモジュールのインストールを始めます。
以下のコマンドを入力してください。

console
$ express --view=pug passport-demo
$cd passport-demo
$ npm install$ npm init -y$ npm install passport
$ npm install passport-facebook
$ npm install express-session

・1行目でプロジェクトを行うpassport-demoディレクトリを作成しています。このディレクトリの名前はなんでも大丈夫です。
・2行目は、1行目で作成したpassport-demoディレクトリに移動しています。
・3行目は依存モジュールのインストールを行なっています。
・4行目は、初期化処理を行い、package.jsonを生成しています。
npm initを行うと普通はどういうパッケージにするか質問がされるのですが、-yオプションを作ることで、すべてyesの回答となり、その質問を省略することができます。
・5、6行目でpassportモジュールとpassport-facebookモジュールをインストールしています。
・7行目は、Expressでセッションを利用できるようにするためのモジュールです。認証した結果をサーバーがセッション情報として保存してくれます。

念の為、この段階で以下のコマンドを実行し、expressがうまく起動できているか確認します。

console
$PORT=8000 npm start

http://localhost:8000/にアクセスしてみてください。
Express
Welcome to Express
という画面が表示されたら、問題ありません。

必要なディレクトリ・ファイルを作る

今回は以下のようなファイル構造にします。

// *は新しく作成するファイル
passport-demo
|- package.json
|- package-lock.json
|- app.js
|- /routes
  |- index.js
  |- login.js *
  |- logout.js *
|- /bin
  |- www
|- /views
  |- login.pug *
  |- index.pug
  |- layout.pug
|- /public
|- /node_modules

app.jspassportモジュールの設定などを書き込むファイルです。
localhost:8000/にアクセスしたときの処理を書き込みます。
login.jslogout.jsは、/login/logoutにアクセスしたときの処理を書き込みます。
wwwはサーバーの起動などを担当します。
login.pug/loginにアクセスした時のログイン画面を表示します。
index.pug/にアクセスした時の画面を表示します。
layout.puglogin.pugindex.pugの基本となる表示を担当します。

続いて、以上のような認証の際に必要なファイルやディレクトリの作成、その記述を行なっていきます。すでに存在しているファイルの作成は不要なので、以下のファイルだけを作成します。

console
$touch routes/login.js
$touch routes/logout.js
$touch views/login.pug

ファイルに処理の記述を行う。

続いては、処理の記述を行なっていきます。
まずは、今あるapp.jsの記述を削除して、app.jsに以下の処理を記述してください。

app.js
varcreateError=require('http-errors');//expressモジュールの読み込みvarexpress=require('express');varpath=require('path');varcookieParser=require('cookie-parser');varlogger=require('morgan');//passportモジュールの読み込みvarpassport=require('passport');//passport-facebookモジュールの読み込みvarStrategy=require('passport-facebook').Strategy;//先ほど作成したアプリIDを変数に代入varFACEBOOK_APP_ID='44923726XXXXXX';//先ほど作成したapp secretを変数に代入varFACEBOOK_APP_SECRET='170dde4751f2ee9d44140b8826457b63';//passportモジュールによるシリアライズの設定passport.serializeUser(function(user,done){done(null,user);});//passportモジュールによるデシリアライズの設定passport.deserializeUser(function(obj,done){done(null,obj);});////passport-facebookモジュールのStarategy設定passport.use(newStrategy({clientID:FACEBOOK_APP_ID,clientSecret:FACEBOOK_APP_SECRET,//facebook認証をするためのページの設定(固定)callbackURL:"http://localhost:8000/auth/facebook/callback",//プロフィールのどの情報を受け取ることができるかの設定profileFields:['id','displayName']},function(accessToken,refreshToken,profile,done){//認証後にdone関数を返すために、process.nextTick関数を利用しているprocess.nextTick(function(){returndone(null,profile);});}));//index.jsの処理を利用するために変数に代入varindexRouter=require('./routes/index');//users.jsの処理を利用するための処理を変数に代入varusersRouter=require('./routes/users');//login.jsの処理を利用するための処理を変数に代入varloginRouter=require('./routes/login');//logout.jsの処理を利用するための処理を変数に代入varlogoutRouter=require('./routes/logout');//expressモジュールを利用するためにapp変数に代入varapp=express();//アプリケーションで利用するミドルウェアの設定app.use(require('morgan')('combined'));app.use(require('cookie-parser')());app.use(require('body-parser').urlencoded({extended:true}));app.use(require('express-session')({secret:'keyboard cat',resave:true,saveUninitialized:true}));app.use(logger('dev'));app.use(express.json());app.use(express.urlencoded({extended:false}));app.use(cookieParser());app.use(express.static(path.join(__dirname,'public')));// viewsディレクトリの設定app.set('views',path.join(__dirname,'views'));app.set('view engine','pug');//passportのの初期化app.use(passport.initialize());//ログイン後のセッション管理の設定app.use(passport.session());//localhost:8000/にアクセスした時にindexRouterの処理がなされる設定app.use('/',indexRouter);//localhost:8000/usersにアクセスした時にusersRouterの処理がなされる設定app.use('/users',usersRouter);//localhost:8000/auth/facebookにGETアクセスした時に認証リクエストを行う設定app.get('/auth/facebook',passport.authenticate('facebook'));//localhost:8000/auth/facebook/callbackにGETアクセスした時に処理が行われる設定app.get('/auth/facebook/callback', //処理が失敗した時のリダイレクト先の設定passport.authenticate('facebook',{failureRedirect:'/login'}),function(req,res){//処理が成功した時のリダイレクト先の設定res.redirect('/');});//localhost:8000/loginにアクセスした時にloginRouter処理がなされる設定app.get('/login',loginRouter);//localhost:8000/logoutにアクセスした時にlogoutRouter処理がなされる設定app.get('/logout',logoutRouter);//404エラーの処理設定app.use(function(req,res,next){next(createError(404));});// エラー処理の設定app.use(function(err,req,res,next){// ローカル環境のみ表示されるエラーの設定res.locals.message=err.message;res.locals.error=req.app.get('env')==='development'?err:{};// エラーページを表示する設定res.status(err.status||500);res.render('error');});module.exports=app;

var FACEBOOK_APP_ID = '4492xxxxxxxx';
var FACEBOOK_APP_SECRET = '170dde4751f2exxxxxxxxxx';
は、先ほど作成した、アプリIDとapp secretをそれぞれ代入します。

続いて、login.jsとlogout.js、そしてindex.jsに記述していきます。

login.js
'use strict';//expressの読み込みvarexpress=require('express');//expressでルーターを使う設定varrouter=express.Router();//localhost:8000/loginにアクセスした際に、login.pugがレンダリングされる処理router.get('/login',function(req,res){res.render('login');});//モジュールのエキスポートmodule.exports=router;
logout.js
'use strict';//expressの読み込みvarexpress=require('express');//expressでルーターを使う設定varrouter=express.Router();//localhost:8000/logoutにアクセスした際に、ログアウトされ、//localhost:8000/にリダイレクトされる処理router.get('/logout',function(req,res){req.logout();res.redirect('/');});//モジュールのエキスポートmodule.exports=router;
index.js
'use strict';varexpress=require('express');varrouter=express.Router();//localhost:8000/にアクセスした際に、index.pugがレンダリングされ、//index.pug内でtitleとuserが使えるようになる処理router.get('/',function(req,res,next){res.render('index',{title:'Express',user:req.user});});module.exports=router;

これでモジュールの処理設定が完了しました。
続いて画面の設定です。
上の処理に合わせて、画面設定をしていきます。

index.pug
extends layout

block content
  h1= title
  p Welcome to #{title}
  if user
    p Hello, #{user.displayName}
    a(href="/logout") Logout
  else
    a(href="/login") Login

#{title}にはindex.jsで渡した、'Express'という文字が入り、
{user.displayName}userには、req.userが入ります。
req.userは、app.jsのStarategy設定でdisplayNameをプロフィールから受け取れるようにしたので、displayNameが利用できます。

login.pug
extends layout

block content
  a(href="/auth/facebook") Login with Facebook

ログインするためのa(href="/auth/facebook")を追加しました。

ログインとログアウトをしてみる

これで処理か完成しました。コンソールにて以下のコマンドを入力し、

console
PORT=8000 npm start

以下のURLにアクセスしてみてください。
http://localhost:8000/

アクセスできたら、以下の画面が表示されると思います。
スクリーンショット 2019-12-03 16.53.34.png

ここでLoginをクリックします。
スクリーンショット 2019-12-03 16.53.41.png

すると、/loginに移動するので、Login with Facebookをクリックします。
スクリーンショット 2019-12-03 16.54.07.png

すると、ログイン画面に出るので、
続いて、自分のFacebookアカウントでログインボタンをクリックし、ログインします。
スクリーンショット 2019-12-03 16.54.16.png
ログインすると、以上のように、Hello 自分の名前と表示されます。
続いて、Logoutをクリックし、ログアウトできるかの確認も行います。

スクリーンショット 2019-12-03 16.54.26.png

無事、最初のログイン画面に移動できたら、ログアウトの完了です。
お疲れ様でした。

おまけ

認証された人しか見れないようにするには、app.jsに以下のように書き込みます。

app.js
//認証者を確かめる関数functionauthenticatedUser(req,res,next){//認証されている人は次の処理が実行される。if(req.isAuthenticated()){returnnext();}//認証されてない人は`/login`にリダイレクトされる。res.redirect('/login');}

/usersを認証者だけが見えるように設定。

app.js
app.use('/users',authenticatedUser,usersRouter);

pkg で node.js を入れたMacで、node.js を消さずに nodebrew + avn を入れようとしたらうまくいかなかったときのメモ

$
0
0

Node.js 7系以下が必要なプロジェクトがあり、node.js のバージョンを切り替える必要があったので nodebrew + avn を使うことにしたときのメモ。
うまくいかなかった時に試したことを一応自分用にメモしておくのがこの記事の目的なので、すっきりわかりやすい記事をご希望の場合は最終項の参考記事を参考にするのが良いかと思います。

1. 方法

前提

  • Mac
  • Node.js をインストーラーからインストール済み
  • nodebrew を homebrew でインストール済み

nodebrew と avn

  • nodebrew だけでも node.js のバージョンは切り替えられる
  • avn を使えば、プロジェクトのルートディレクトリに置いた .node-versionのバージョンに合わせて node.js のバージョンを切り替えてくれる

その他切り替える方法として anyenv (参考 https://www.to-r.net/media/anyenv/)というのもあるらしいです。

手順

  1. グローバルにavnとavn-nodebrewをインストール&セットアップする。
terminal
npm install-g avn avn-nodebrew
avn setup
  1. プロジェクトのルートディレクトリに .node-versionを作成する。 作成したファイルには使いたい node.js のバージョンを以下のようにセマンティックバージョニングで書く。
.node-version
v6.17.1
  1. ターミナルでプロジェクト内に cdで移動して、以下のように activatedが表示されていればOK。
terminal
avn activated v6.17.1 (avn-nodebrew v6.17.1)

2. Trouble Shooting

だいぶ完結に「1.」にまとめましたが、実際は結構時間がかかりました。
やったことで覚えていることを時系列に関係なくメモだけしておきます。

node -vをして確認したら切り替わってなかった

↑のようにバージョンが変わったように思ったけど、 node -vで念のためバージョンを確認すると変わっていなかった...。
とりあえず、 一旦全部削除してみようと思い、一からやり直してみた。

terminal
v10.17.0

pkg で入れた node.js を削除(ついでに npm も削除)

MacにpkgでインストールしたNode.jsをアンインストールする手順を参考に削除。
以下を一行ずつ実行していく。(>がでてきても次の行を>につづけてペーストしてenterを押す)

terminal
lsbom -f-l-s-pf /var/db/receipts/org.nodejs.node.pkg.bom \
| while read i;do
  sudo rm /usr/local/${i}done
sudo rm-rf /usr/local/lib/node \
     /usr/local/lib/node_modules \
     /var/db/receipts/org.nodejs.*

npm も削除する場合は以下で。

terminal
sudo rm-rf ~/.npm

node.js、npm が削除されてるかどうかは以下のコマンドでチェック(バージョンがでてこなければOK)

terminal
node -v
npm -v

nodebrew を入れ直してみる

nodebrew を削除する

Nodebrew本体を削除する方法を参考に Finder or ターミナルから直接 .nodebrewディレクトリを削除。
(隠しファイルの表示方法はこちら

ただ、homebrew で nodebrew を入れた場合は上記じゃ消せないときがあるので、その場合は下記のコマンドで削除。

terminal
brew uninstall nodebrew

nodebrew を入れ直す

GitHubページの通り、curlコマンドでインストール。
(homebrew でも入れれますが、削除するときにちょっと面倒だったのでcurlで入れました)

terminal
curl -L git.io/nodebrew | perl - setup

インストールしたら、パスを通す。(GitHubページには .bashrc or .zshrcと書かれてます)
私は .bashrcに書きました。

export PATH=$HOME/.nodebrew/current/bin:$PATH

bashrcを更新しても、ターミナルを再起動しただけではシェルの設定が反映されないので、以下のコマンドを叩いて反映させる。

terminal
source ~/.bashrc
!!注意!!

上記を実行しただけだと
「PhpStormのTerminalでは nodebrew コマンドが使えるのに、App のターミナルで新しいタブを開いても nodebrew コマンドが使えない...」
という状況に陥りました。

これは実行系統によって読み込む設定ファイルが微妙に異なることが原因だそう。(両者のターミナルで読み込む設定が違ったみたい)
そこで、 .bashrcの更新内容が今後自動的に .bash_profileに反映されるようにするため、 .bash_profileに下記コマンドを追加するとどちらでも nodebrew コマンドが使えるようになりました。

.bash_profile
source ~/.bashrc

(↓あたりを参考に)
https://qiita.com/hiesiea/items/860c42a96b031f929b94
https://qiita.com/magicant/items/d3bb7ea1192e63fba850

avn could not activate node v6.17.1と出る

nodebrew に activate したい node.js がインストールされていないのが原因でした。

nodebrew にインストール済みのバージョンの確認

terminal
nodebrew ls

nodebrew にバージョンをインストールする

terminal
nodebrew install-binary 6.17.1

参考

avnのGitHubページ
Nodebrewとavnを使ってNode.jsのバージョン切り替えを自動化する
ディレクトリごとに異なるバージョンのnodeを使いたいのでavnを使った話

公式インストーラーからインストールしたNode.jsを削除する方法(macOS)

$
0
0

概要

Node.js FoundationはMacOS向けに公式インストーラーを用意している。

Screen Shot 2019-12-02 at 17.30.38.png

アンインストールの方法についての公式の見解が特に見当たらなかったので、推測しながら試した結果うまくいったので、記録として残す。
もし間違っている点があれば教えてほしいです!

対象者

  • Node.jsの公式インストーラーをインストールしたが、nvmなどのバージョン管理ツールに切り替えたい人
  • 既にnvmなどのバージョン管理ツールを使用しているが、過去にNode.jsの公式インストーラーを使用した覚えのある人

背景

興味本位から、まっさらなMacOSに公式インストーラーでNode.jsをインストールしたところ、
「これどうやってアンインストールするんだ...?」となったため。

アンインストール方法

まず、Node.jsインストール後、以下のようなメッセージが出ることをご存知だろうか。
Screen Shot 2019-12-03 at 13.10.51.png

つまりこういうことですね。

Node.jsを/usr/local/bin/nodeにインストールしたで!
npmを/usr/local/bin/npmにインストールしたで!

つまり、このインストーラー経由でインストールしたNode.jsは、/usr/local/bin/nodeを削除することでアンインストールできるということです。(という推測を立てた)
というわけで、以下のコマンドを実行。

rm -f /usr/local/bin/node

無事成功しました!↓
成功

npmをアンインストールしたい場合も同様。

rm -f /usr/local/bin/npm

さいごに

既にバージョン管理ツールを用いてNode.jsを管理している場合でも、そもそもNode.jsの格納先が違うのでこの方法で対処できるかと思います。
(nvmにて検証済み。保証はしません)

多少知識があればバージョン管理ツールを使うのが良いと思いますが、初学者にNode.jsをインストールしてほしい!けどアンインストール方法も知っておいてほしい!という場面などでご活用ください。

nodejs v12(LTS)におけるasync, awaitを用いたstream処理

$
0
0

nodejs v12(LTS)におけるasync, awaitを用いたstream処理

QualiArts Advent Calendar 2019、3日目の記事になります。

はじめに

2019年10月21日にnodejs v12のLTS版が公開されました。

nodeは奇数バージョンが開発版、偶数バージョンが安定版となるため、v11以降の今まで実プロジェクトだと利用しにくかった機能がこれによりいくつか使えるようになりました。

そのなかでもasync-generatorsやfor-await-of loops構文が大手を振って使えるようになったことにより、stream関連の処理が大きく変わると感じたため、すでにいくつか紹介されている記事も有りますが、この機会に改めて紹介したいと思います。
また、最後に簡単なサンプル処理も記述しているので、ご参考いただければ幸いです。

for-await-of loops

今までstreamはeventEmitterを利用し、発火したeventをトリガーに処理を記述していました。
for-await-of loopsを用いると下記のようにわかりやすくかけるようになります。
for-await-of自体は単純にfor-ofのasyncも利用可能になったものとなります。

for-await-of_loops1.js
constfs=require('fs');constreader=async(rs)=>{forawait(constchunkofrs){console.log(chunk);}};(asyncfunction(){constrs=fs.createReadStream('./input.txt',{encoding:'utf8'});awaitreader(rs);})();
input.txt
abcde
fghij
klmno
pqrxy
z

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

従来だと、下記のように終了イベントをpromise化するなどで、全体のpromise化などは簡単にできますが、イテレータ内部のchunk単位でpromise化を行う場合非常に可読性が悪くなってしまっていました。
(v10以降であれば下記のようにstream.finishedをpromisifyすることで全体のpromise化は簡略可能です)

これが上記のように簡単に記述できるようになったのは非常にやりやすくなったと感じます。

for-await-of_loops2.js
constfs=require('fs');conststream=require('stream');constutil=require('util');constfinished=util.promisify(stream.finished);constmsleep=util.promisify(setTimeout);// streamを用いた場合の処理(全体終了部分のみのpromise化)constreader1=async(rs)=>{rs.on('data',(chunk)=>{console.log(chunk);});awaitfinished(rs);// stream.finishedを使わない場合下記のようなpromiseを生成する// await new Promise((resolve, reject) => {//   rs.once('finished', (chunk) => {//     return resolve(data);//   });//   rs.once('error', (err) => {//     return reject(err);//   });// });};// streamを用いた場合の処理(chunk単位でのpromise化)constreader2=async(rs,iterator)=>{letbuffer='';rs.on('data',(chunk)=>{buffer=chunk;rs.pause();});letisEnd=false;rs.once('end',()=>{isEnd=true;});leterror;rs.once('error',(err)=>{error=err;});while(true){if(error){throwerror;}if(buffer){awaititerator(buffer);buffer='';}elseif(isEnd){return;}elseif(rs.isPaused()){rs.resume();}// 非同期メソッドがないと無限ループしてしまうためawaitmsleep(0);}}(asyncfunction(){constrs1=fs.createReadStream('./input.txt',{encoding:'utf8'});awaitreader1(rs1);constrs2=fs.createReadStream('./input.txt',{encoding:'utf8'});awaitreader2(rs2,async(chunk)=>{console.log(chunk);});})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z
abcde
fghij
klmno
pqrxy
z

async-generators

今までは同期メソッドでしか使えなかったyieldがasyncにも対応しました。
async function*でasyncIteratorのジェネレータメソッドを生成でき、await対応したnextメソッドを呼び出すことができます。
(nextで呼び出した場合返り値はObjectになります)

async-generators1.js
constutil=require('util');constmsleep=util.promisify(setTimeout);asyncfunction*generate(){for(leti=1;i<=3;i++){awaitmsleep(1000);yieldi;}}constasyncIterator=generate();(async()=>{console.log(awaitasyncIterator.next());console.log(awaitasyncIterator.next());console.log(awaitasyncIterator.next());console.log(awaitasyncIterator.next());})();

実行結果

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }

こちらはfor-await-of loopsも利用可能です。
こちらを利用すると簡単にラグのあるstreamデータの生成が可能になります。

async-generators2.js
constutil=require('util');constmsleep=util.promisify(setTimeout);asyncfunction*generate(){for(leti=1;i<=3;i++){awaitmsleep(1000);yieldi;}}constasyncIterator=generate();(async()=>{forawait(constvofasyncIterator){console.log(v);}})();

実行結果

1
2
3

行ごとにawait処理を行うサンプル

上記の機能が実装されたことで、行ごとのように一定windowずつstreamで非同期メソッドを実行する処理が非常に簡単にかけるようになりました。
下記は得られたstreamを、行ごとに非同期メソッドを実行する場合のサンプルになります。
可読性重視&行単位でeventループが回るため、パフォーマンスがシビアな場合は別途実装することをおすすめします。

readlineモジュールを利用した場合

line-reader1.js
constfs=require('fs');constreadline=require('readline');constutil=require('util');constmsleep=util.promisify(setTimeout);constasyncLineReader=async(iterater)=>{constrl=readline.createInterface({input:fs.createReadStream('input.txt',{encoding:'utf8'}),crlfDelay:Infinity});forawait(constlineofrl){awaititerater(line);}}(async()=>{awaitasyncLineReader(async(line)=>{awaitmsleep(100);console.log(line);});})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

解説

こちらはreadlineモジュールを利用したものになります。
以前も行ごとに処理を行えたのですが、非同期メソッドの実行はできませんでした。
v1.12からasyncIteratorに対応したことで、上記のように簡単に非同期メソッドが実行できるようになりました。

stream(for-await-of利用)

line-reader2.js
constfs=require('fs');constutil=require('util');constmsleep=util.promisify(setTimeout);// streamを用いた場合の処理(for-await-of使用)constasyncLineReader=async(iterater)=>{constrs=fs.createReadStream('./input.txt',{encoding:'utf8'});letbuffer='';forawait(constchunkofrs){buffer+=chunk;constlist=buffer.split('\n');// 最後の要素は改行が含まれているわけではないため、bufferに戻すbuffer=list.pop();for(leti=0;i<list.length;i++){awaititerater(list[i]);}}if(buffer){// 終了時にbufferに残っている文字列もiteratorにわたすawaititerater(buffer);}}(async()=>{awaitasyncLineReader(async(line)=>{awaitmsleep(100);console.log(line);});})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

解説

こちらはstreamのfor-await-ofを利用したものになります。
イベントループの回数が他と比べて半分以下なので、このなかでは一番パフォーマンスが良いです。
実装を合わせるために1行ずつ処理していますが、こちらで複数行制御してlistをiteratorに渡すような実装が実利用だと良いかもしれません。
比較的簡単に可読性良く記述できるようになっているかと思います。

stream(for-await-of未使用)

line-reader2.js
constfs=require('fs');constutil=require('util');constmsleep=util.promisify(setTimeout);// streamを用いた場合の処理(for-await-of未使用)constasyncLineReader=async(iterator)=>{constrs=fs.createReadStream('./input.txt',{encoding:'utf8'});letbuffer='';letrows=[];rs.on('data',(chunk)=>{buffer+=chunk;constlist=buffer.split('\n');buffer=list.pop();if(list.length){rows.push(...list);rs.pause();}});letisEnd=false;rs.once('end',()=>{isEnd=true;});leterror;rs.once('error',(err)=>{error=err;});while(true){if(error){// errorがあれば終了throwerror;}if(rows.length){for(leti=0;i<rows.length;i++){awaititerator(rows[i]);}rows=[];}elseif(isEnd){if(buffer){// 終了時にbufferに残っている文字列もiteratorにわたすawaititerator(buffer);}return;}elseif(rs.isPaused()){rs.resume();}// 非同期メソッドがないと無限ループしてしまうため、setImmediate代わりに実行awaitmsleep(0);}}(async()=>{awaitasyncLineReader(async(line)=>{awaitmsleep(100);console.log(line);});})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

解説

こちらはstreamのfor-await-ofの未使用版になります。
かなり複雑になり可読性も落ちている事がわかります。
ただ、async, awaitが対応しているv8以降であれば利用可能なため、nodeのバージョン次第では利用できるかもしれません。

まとめ

これらの機能の追加により、async, awaitを用いたstream処理が非常に簡潔に記述できるようになりました。
v1.12LTSに上げることで非常にstream周りが記述しやすくなっているため、この機会にぜひ試してみてはどうでしょうか?

nvm チートシート

$
0
0

前書き

この記事は オープンロジアドベントカレンダー2019の3日目です。
みなさんはNode.js のバージョンマネージャーは使っていますか?
弊社では3年ほど前は 何故か nodebrewをインストールすることを推奨されており、
私は愚直にそれを利用していたのですが、LTSを入れるコマンドが存在せず、
使い勝手の悪さを感じていたので、最近は nvmというバージョン管理マネージャに乗り換えました。

使い方については、 GitHubの Readme と nvm --helpの出力結果を読めばそれで十分なのですが、その都度、英語を読むのは面倒なので、以下に個人的によく使うであろうものを取捨選択してチートシートとしてまとめておきます。
本稿での nvm のバージョンは v0.35.1 です。
.nvmrcについては別の方の記事を参照してください。本稿では言及しません。

チートシート

前提

  • 間違ったオプションや文字列を入力したら、nvm --helpの内容を表示される。
    • nvmと打つだけでも nvm --helpの内容が表示される。
  • 出力結果に色がつくものは --no-colorsオプションをつけるとプレーンテキストで出力される。
  • バージョン指定する際に、オプションに --ltsで最新のLTS を指定できる。(2019年12月現在はv12の最新バージョン)
  • 特定のLTSを指定したい場合は、--lts=<LTS name>で 指定できる。
    • --lts=carbon -> v8の最新
    • --lts=dubnium -> v10の最新
    • --lts=erbium -> v12の最新

基本

ヘルプを表示する

$ nvm --help

nvmのバージョンを表示する

$ nvm --version

インストールされているNode.jsのバージョンを表示する

$ nvm version <version>

バージョンを指定しない場合は nvm currentと同じ挙動

現在利用しているNode.jsのバージョンを表示する

$ nvm current

バージョンを指定しない場合、現在の Node.jsのバージョンを表示し、バージョンを指定した場合はローカルにインストールされている最新のバージョンを表示する

指定バージョンのNode.js をインストール

$ nvm install<version>

指定したバージョンのNode.js を ダウンロードしてインストールして利用するバージョンを切り替える。

すでにインストール済みの指定したバージョンのNode.js から node_modules を引き継いてインストール

$ nvm install<version> --reinstall-packages-from=<version>

デフォルトパッケージをスキップして指定したバージョンのNode.jsをインストール

$ nvm install<version> --skip-default-packages

指定したバージョンのNode.jsをインストールした後にそのNode.jsで利用できる最新のnpmをインストール

$ nvm install<version> --latest-npm

指定したNode.js のバージョンをアンインストール

$ nvm uninstall <version>

指定したNode.jsのバージョンを利用する

$ nvm use [--silent]<version>

--silentオプションを利用すると実行結果が表示されない。

インストールされているNode.jsの一覧を出力

nvm ls [<version>]

バージョンを指定した場合は指定したバージョンがすべて表示される

リモートに登録されているNode.jsのバージョンの一覧を出力

nvm ls-remote [<version>]

現在利用しているNode.jsのバージョンでの最新のnpmへのアップグレード

nvm install-latest-npm

バージョン指定したグローバルのnpmバッケージを現在のバージョンにインストール

nvm reinstall-packages <version>

指定したNode.jsのバージョンのインストール先のパスを表示する。

nvm which [current | <version>]

指定したNode.jsのバージョンでコマンドを実行する

nvm exec [--silent] <version> [<command>]

--silentオプションを利用すると実行結果が表示されない。

引数付きで指定バージョンのNode.jsを実行

nvm run [--silent] <version> [<args>]

--silentオプションを利用すると実行結果が表示されない。

便利な使い方

最新のLTSをインストールして、現在利用しているNode.jsのパッケージもインストール

nvm install"lts/*"--reinstall-packages-from=current

これでLTSを最新にしたときに現在グローバルにインストールされている node_modulesも手軽にインストールできます。

参考リンク

GitHub/nvm-sh/nvm#usage
GitHub/nvm-sh ヘルプコマンドの実装

【自分用メモ】supertestとpassport-stubをmochaテストに組み合わせる

$
0
0

supertestとは

supertestはmochaと組み合わせて使うのですが、ExpressのRouterモジュールのテストを行うことができます。
例えば以下の例では、/にアクセスしたらindexRouterが処理されるかテストしてくれます。
もちろん、/login/logoutもテストしてくれます。

app.js
app.use('/',indexRouter);app.use('/login',loginRouter);app.use('/logout',logoutRouter);

passport-stubとは

passport-stubは、passportモジュールを利用した認証システムを、テストする際に役に立ちます。
例えば、
「facebook認証などのテストをしたいけど、facebookアカウントを持っていない!」
といった時に役に立ちます。

テストの例

test.js
//supertestの読み込みconstrequest=require('supertest');//supertestで使う、app.jsの読み込みconstapp=require('app');//passport-stubの読み込みconstpassportStub=require('passport-stub');//ログイン(/login)のテストであることを明示describe('/login',()=>{//before、afterはmochaの機能before(()=>{//テストの前にpassportstubモジュールでログインpassportStub.install(app);//'testuser'としてログインpassportStub.login({username:'testuser'});});after(()=>{//テストの後にpassportstubモジュールでログアウトpassportStub.logout();passportStub.uninstall(app);});//以下の記法は、supertestの記法 //テストの内容を指定it('ログインのためのリンクが含まれる',(done)=>{//request(app).get('/login') で、 /login への GETリクエストを作成request(app).get('/login')//文字列を2つ引数として渡すとヘッダのテスト.expect('Content-Type','text/html; charset=utf-8')//正規表現を1つ渡すとHTMLのテスト.expect(/<a href="\/auth\/facebook"/)//期待されるステータスコードの整数と、テスト自体の引数に渡されるdone 関数を渡すと、レスポンスヘッダのテスト.expect(200,done);});it('ログイン時はユーザー名が表示される',(done)=>{request(app).get('/login').expect(/testuser/).expect(200,done);});});//ログアウト(/logout)のテストであることを明示describe('/logout',()=>{ //テスト内容を明示it('ログアウト後に / にリダイレクトされる',(done)=>{////request(app).get('/logout')で、/logoutへのGETリクエストを作成request(app).get('/logout')// `/`へリダイレクトされるかのテスト.expect('Location','/')    // ステータスコードがリクエストであるかのテスト.expect(302,done);});});

describeitbeforeafterはmochaの書き方。
request(app).get.expectはsupertestの書き方
passportStub.install(app)passportStub.loginはpassport-stubの書き方

テスト結果

スクリーンショット 2019-12-03 22.47.50.png

Angularスキル獲得のために始めたこと、始めること

$
0
0

お仕事だったり同期と作ったアドベントカレンダーだったりのおかげで、Angularを触る機会を得た小生でございます。
今までフロントどころか、Webアプリの制作もしたことがなかったので、これをいいことにいろいろと勉強していってる最中です。

Angularを触るにあたって何を知っていたか

  • HTML
  • CSS
  • Javascript
  • Node.js

HTML、CSSはお猿さんと同じくらいの知識がありました。
JavascriptはほぼNode.js触ってから覚えた感じ。
元々プログラミング経験があったので、ここらへんはなんとか理解しつつ進めております。

Angularを理解するためには

特に2020年のフロントエンドマスターになりたければこの9プロジェクトを作れはめちゃくちゃ面白いです。
Angularに限らず、フロントのフレームワークの基礎押さえたなら、それぞれ作っていくべきだと思います。
元記事ではフロントエンドマスターになるために様々なフレームワークを紹介していますが、まずは一本極めていくのが自分のやり方なので、Angularで絞ってやっていきます。

始めたこと:公式チュートリアル制覇

入門チュートリアルでは、Angularがどんな感じで動いているのかを理解できました。
基礎チュートリアルでは、コンポーネント指向に置いて説明がされている印象を受けました。
コピペだけで作れなくはないですが、用語が分からずともしっかり説明を読んで、ちょこちょこコードをいじったりするとより理解が深まります。

始めたこと:Build a movie search app

とっかかりとして、Angularで映画情報を検索するWebアプリを作りました。できたものはこんな感じです。

movielist01.png

ガッツリ参考URL載せてるくせに、実は一度も読みに行ってません…
貼られてたスクショを元に、機能を想像しながら、真似た物を作ってみました。

検索フォームにキーワードを入力すると…

movielist02.png

関連する映画が表示されます。

movielist03.png

ページ移動とかもちゃんと機能します。どれでもいいので映画をクリックすると、

movielist05.png

このような形で、映画の詳細情報がでます。

Angularの勉強は楽しいのですが、なにぶんCSSをしっかり書いたことがないもので…
詳細情報ページだけ、間に合わせのtableで凌いでます。
(検索フォームはなぜか真ん中に来ないのでおこです😡)

始めたこと:AngularでWebアプリを設計するには

もうこれはWebデザイン全般に言えることかもしれないんですけど、
設計図を書きましょうもっというと、画面図を書きましょうですね

movielist06.png

Angularはコンポーネント指向でアプリを作るので、どのコンポーネントがどの部分に来るか、明確にイメージしていないとすぐこんがらがります(一人で作る場合)
最初に必要なコンポーネントをがーっと作って、その後設計を考えながら組み立てるのも悪くないですが、あとからたくさん修正が必要そうになるので、概要くらいは決めておいた方が良いです。

始めること:アウトプット、アウトプット、アウトプット

やっぱり手を動かさないと始まらない、ということで当面の目標はサンプルアプリを作り続けるです。
嬉しいことにコードを書くスピードが上がっているのを実感できているので、アドベントカレンダー最終日までにあと2つはサンプルを作りたいと思います。
併せて、Bootstrapについても勉強を始めようと思います。
とりあえず次回の記事は、今回紹介したサンプルアプリの詳細と、次に作るアプリの設計について書いていきます。


SeleniumでSortableJS系ライブラリのDrag&Dropをテストする

$
0
0

前置き

前回の記事で、Vue.Draggableを使ったコンポーネントのドラッグ&ドロップを実行するCypressのテストコードについて書きました。
これをSeleniumで書いたらどうなるだろうと思い試してみたところCypress以上にハマったので、解決方法を記録しておきます。1

本記事内のドラッグ&ドロップのテストコードは、Vue.Draggableに限らずSortableJSベースのライブラリなら概ね動くものになります。
以下の公式サイトのデモにて検証しています。(2019/12/3時点)

※react-sortablejsと他の3種類とでは若干テストコードが変わります。
本文内ではSortableJSとreact-sortablejsのデモページに対するテストコードを掲載しています。使用言語はNode.jsとRubyです。

環境

  • OS: Mac OS X 10.14.6 Mojave
  • Node.js
    • Node.js: v12.13.1
    • selenium-webdriver: 4.0.0-alpha.5
    • Mocha: 6.2.2
  • Ruby
    • Ruby: 2.6.5
    • selenium-webdriver: 3.142.6
    • minitest: 5.13.0
  • Browser
    • Google Chrome: 78.0.3904.108(Official Build)
    • chromedriver: 78.0.3904.105(Homebrewにてインストール)
    • Firefox: 70.0.1 (64 ビット)
    • geckodriver: 0.26.0(Homebrewにてインストール)
    • Safari: 13.0.3
    • safaridriver: 1.0
  • Library(公式のデモで使用されていると思われるバージョン)
    • SortableJS: 1.10.0-rc3
    • Vue.Draggable: 2.23.2
    • react-sortablejs: 1.5.1
    • ngx-sortablejs: 3.1.3

ドラッグ&ドロップが動作するテストコード(Node.js版)

SortableJSの公式のデモページにアクセスし、Simple list example の Item 1 を Item 2 にドラッグ&ドロップして、テキストが入れ替わることを確認するテストコードです。
テストフレームワークはMochaを、アサーションはNode.jsのassertモジュールを使用しています。
マニュアル操作では以下のGIFアニメのようになります。
sortablejs.gif

test.js
const{Builder,By}=require('selenium-webdriver')constassert=require('assert')describe('Drag and Drop test',function(){// ブラウザの起動を待つあいだにMochaがタイムアウトしてしまうのを防止this.timeout(20*1000)letdriverbeforeEach(async()=>{driver=awaitnewBuilder().forBrowser('chrome')// Chromeを使う場合// .forBrowser('firefox') // Firefoxを使う場合// .forBrowser('safari')  // Safariを使う場合.build()})afterEach(async()=>{awaitdriver.quit()})it('SortableJS',async()=>{// SortableJSの公式デモページにアクセスawaitdriver.get('https://sortablejs.github.io/Sortable/#simple-list')// ドラッグ&ドロップの対象を含むdiv要素のリストを取得letelementselements=awaitdriver.findElements(By.css('div#example1 > div.list-group-item'))// ドラッグ元(Item 1)とドロップ先(Item 2)のdiv要素を取得constsourceElement=awaitelements[0]consttargetElement=awaitelements[1]// ドラッグ&ドロップを実行する関数の呼び出しawaitsimulateDragAndDrop(sourceElement,targetElement)// Item 1 と Item 2 が入れ替わったことを確認elements=awaitdriver.findElements(By.css('div#example1 > div.list-group-item'))assert.strictEqual(awaitelements[0].getText(),'Item 2')assert.strictEqual(awaitelements[1].getText(),'Item 1')})/**
   * ドラッグ&ドロップを実行する関数
   */asyncfunctionsimulateDragAndDrop(sourceElement,targetElement){awaitdriver.executeScript(asyncargs=>{// dragoverイベントの発火位置を計算consttargetRect=args.targetElement.getBoundingClientRect()consttargetPositionX=(targetRect.left+targetRect.right)/2consttargetPositionY=(targetRect.top+targetRect.bottom)/2// ドラッグ&ドロップに必要な各イベントのインスタンスオブジェクトを作成constpointerDownEvent=newPointerEvent('pointerdown',{bubbles:true,cancelable:true,})constdragStartEvent=newMouseEvent('dragstart',{bubbles:true,})constdragOverEvent=newMouseEvent('dragover',{bubbles:true,clientX:targetPositionX,clientY:targetPositionY,})constdropEvent=newMouseEvent('drop',{bubbles:true,})// sleep処理用の関数を定義constsleep=msec=>newPromise(resolve=>setTimeout(resolve,msec))// イベントの発火args.sourceElement.dispatchEvent(pointerDownEvent)args.sourceElement.dispatchEvent(dragStartEvent)awaitsleep(1)args.targetElement.dispatchEvent(dragOverEvent)args.targetElement.dispatchEvent(dropEvent)},{sourceElement,targetElement})}})

テストコードの解説

SortableJSを使用した要素のドラッグ&ドロップを実行するには、以下の4つのイベントの発火が必要になります。

  1. pointerdown
  2. dragstart
  3. dragover
  4. drop

selenium-webdriver本体にもドラッグ&ドロップ機能は実装されていますし(公式ドキュメント)、ドラッグ&ドロップ操作のための外部ライブラリもいくつか公開されています。
しかし試してみた範囲では、いずれも何かしらのイベントの発火が足りずドラッグ&ドロップは期待通りに動作しませんでした。

Cypressのように必要なイベントを個別に発火させることができればよさそうなのですが、selenium-webdriverにはそういった機能はないようです。
そのため、素のJavaScriptでイベントを発火させる処理を書き、それをselenium-webdriverの executeScript()を使って実行するという方法をとることになりました。

JavaScriptを書く際のポイントが何点かありましたので説明します。

ポイント1
dragover イベントのインスタンス作成時のコンストラクタで、イベントを発火させる位置を指定しておく必要があります。
getBoundingClientRect()でドロップ対象要素の viewport に対する位置を取得し、それをもとに対象要素の中央にあたる位置を計算して、その値をコンストラクタの clientXclientYに設定しました。

test.js
// dragoverイベントの発火位置を計算consttargetRect=args.targetElement.getBoundingClientRect()consttargetPositionX=(targetRect.left+targetRect.right)/2consttargetPositionY=(targetRect.top+targetRect.bottom)/2// 中略// dragoverイベントのコンストラクタでイベントの発火位置を指定constdragOverEvent=newMouseEvent('dragover',{bubbles:true,clientX:targetPositionX,clientY:targetPositionY,})

ポイント2
dragstart と dragover を順に dispatchEvent する際、あいだに sleep を挟む必要があります。
sleep が必要になる根本的な理由がまだ突き止められていないのですが、ひとまず動いたのでよしとしています。

test.js
// sleep処理用の関数を定義constsleep=msec=>newPromise(resolve=>setTimeout(resolve,msec))// イベントの発火args.sourceElement.dispatchEvent(pointerDownEvent)args.sourceElement.dispatchEvent(dragStartEvent)// ここでsleepが必要awaitsleep(1)args.targetElement.dispatchEvent(dragOverEvent)args.targetElement.dispatchEvent(dropEvent)

ポイント3
MacのSafariをテスト対象とする場合ですが、Safariでは DragEventをnewできません。(Chrome、Firefoxではできます)
そのためドラッグ系のイベントでも MouseEventを使っています。
MDNにも Can I use...にもSafariはDragEventをサポートしていると書かれているのですが、Safariのコンソールで直接コードを叩いてみても ReferenceError: Can't find variable: DragEventと返ってきてしまいました。

test.js
// Safariでは new DragEvent と書くと動作しないconstdragStartEvent=newMouseEvent('dragstart',{bubbles:true,})constdragOverEvent=newMouseEvent('dragover',{bubbles:true,clientX:targetPositionX,clientY:targetPositionY,})constdropEvent=newMouseEvent('drop',{bubbles:true,})

ポイント4
前置きにも書きましたがreact-sortablejsのデモの場合、前出のテストコードではドラッグ&ドロップが動作しません。

react-sortablejsでは、dragstart イベントが発火した際に、イベントターゲットとなった要素が2つに増えるという挙動をします。
react-sortablejs.gif
この要素の増加により、リスト内でのドロップ先要素の index がずれてしまい、目的のドロップ先に dragover できなくなるケースが発生します。それに対応するため処理に手を加えなければなりません。

要素数の増加に対応したテストコードの例が以下になります。

ドラッグ&ドロップが動作するテストコード(Node.js + react-sortablejs版)

react-sortablejsの公式のデモページにアクセスし、Simple List の List Item 1 を List Item 2 にドラッグ&ドロップしてテキストが入れ替わることを確認するテストコードです。

記事が長くなるので折りたたみます。

react-sortablejsのテストコード例
test.js
// requireやbefore/after部分は前出のテストコードと共通it('react-sortable',async()=>{// react-sortablejsの公式デモページにアクセスawaitdriver.get('http://sortablejs.github.io/react-sortablejs/#container')letelements,sourceElementIndex,targetElementIndex// ドラッグ&ドロップの対象を含むli要素のリストを取得elements=awaitdriver.findElements(By.css('ul.block-list > li'))// ドラッグ元(List Item 1)とドロップ先(List Item 2)のli要素の、リスト内でのindexを定義sourceElementIndex=0targetElementIndex=1// ドラッグ&ドロップを実行する関数の呼び出しawaitsimulateDragAndDropForReact(elements,sourceElementIndex,targetElementIndex)// List Item 1 と List Item 2 が入れ替わったことを確認elements=awaitdriver.findElements(By.css('ul.block-list > li'))assert.strictEqual(awaitelements[0].getText(),'List Item 2')assert.strictEqual(awaitelements[1].getText(),'List Item 1')})/**
   * ドラッグ&ドロップを実行する関数
   */asyncfunctionsimulateDragAndDropForReact(elements,sourceElementIndex,targetElementIndex){awaitdriver.executeScript(asyncargs=>{// dragoverイベントの発火位置を計算consttargetRect=args.elements[args.targetElementIndex].getBoundingClientRect()consttargetPositionX=(targetRect.left+targetRect.right)/2consttargetPositionY=(targetRect.top+targetRect.bottom)/2// ドラッグ&ドロップに必要な各イベントのインスタンスオブジェクトを作成constpointerDownEvent=newPointerEvent('pointerdown',{bubbles:true,cancelable:true,})constdragStartEvent=newMouseEvent('dragstart',{bubbles:true,})constdragOverEvent=newMouseEvent('dragover',{bubbles:true,clientX:targetPositionX,clientY:targetPositionY,})constdropEvent=newMouseEvent('drop',{bubbles:true,})// sleep処理用の関数を定義constsleep=msec=>newPromise(resolve=>setTimeout(resolve,msec))// ドラッグ元の要素よりもドロップ先の要素が要素リストの後ろにある場合、// dragover発火時にイベントターゲットとなるドロップ先要素のindexを+1するconstadjustIndex=args.sourceElementIndex<args.targetElementIndex?1:0// イベントの発火args.elements[args.sourceElementIndex].dispatchEvent(pointerDownEvent)args.elements[args.sourceElementIndex].dispatchEvent(dragStartEvent)awaitsleep(1)args.elements[args.targetElementIndex+adjustIndex].dispatchEvent(dragOverEvent)args.elements[args.targetElementIndex].dispatchEvent(dropEvent)},{elements,sourceElementIndex,targetElementIndex})}

ドラッグ&ドロップが動作するテストコード(Ruby版)

Rubyでは以下のように書くことができます。2
テストフレームワークはminitestを使用しています。

記事が長くなるので折りたたみます。

Rubyのテストコード例
test.rb
require'selenium-webdriver'require'minitest/autorun'describe'Drag and Drop test'dodriver=nilbeforedodriver=Selenium::WebDriver.for:chrome# Chromeを使う場合# driver = Selenium::WebDriver.for :firefox # Firefoxを使う場合# driver = Selenium::WebDriver.for :safari  # Safariを使う場合endafterdodriver.quitendit'SortableJS'do# SortableJSの公式デモページにアクセスdriver.get'https://sortablejs.github.io/Sortable/#simple-list'# ドラッグ&ドロップの対象を含むdiv要素のリストを取得elements=driver.find_elements(:css,'div#example1 > div.list-group-item')# ドラッグ元(Item 1)とドロップ先(Item 2)のdiv要素を取得sourceElement=elements[0]targetElement=elements[1]# ドラッグ&ドロップを実行するメソッドの呼び出しsimulateDragAndDrop(sourceElement,targetElement,driver)# Item 1 と Item 2 が入れ替わったことを確認elements=driver.find_elements(:css,'div#example1 > div.list-group-item')assert_equal(elements[0].text,'Item 2')assert_equal(elements[1].text,'Item 1')endend## ドラッグ&ドロップを実行するメソッド#defsimulateDragAndDrop(sourceElement,targetElement,driver)driver.execute_script(<<-EOL,sourceElement,targetElement)
    (async (sourceElement, targetElement) => {
      // dragoverイベントの発火位置を計算
      const targetRect = targetElement.getBoundingClientRect()
      const targetPositionX = (targetRect.left + targetRect.right) / 2
      const targetPositionY = (targetRect.top + targetRect.bottom) / 2

      // ドラッグ&ドロップに必要な各イベントのインスタンスオブジェクトを作成
      const pointerDownEvent = new PointerEvent('pointerdown', {
        bubbles: true,
        cancelable: true,
      })

      const dragStartEvent = new MouseEvent('dragstart', {
        bubbles: true,
      })

      const dragOverEvent = new MouseEvent('dragover', {
        bubbles: true,
        clientX: targetPositionX,
        clientY: targetPositionY,
      })

      const dropEvent = new MouseEvent('drop', {
        bubbles: true,
      })

      // sleep処理用の関数を定義
      const sleep = msec => new Promise(resolve => setTimeout(resolve, msec))

      // イベントの発火
      sourceElement.dispatchEvent(pointerDownEvent)
      sourceElement.dispatchEvent(dragStartEvent)
      await sleep(1)
      targetElement.dispatchEvent(dragOverEvent)
      targetElement.dispatchEvent(dropEvent)

    })(arguments[0], arguments[1])
  EOLend

テスト対象がreact-sortablejsの場合は、Node.js版と同じように手を加える必要があります。(テストコード例は割愛)

後書き

個人的にはドラッグ&ドロップの挙動自体はUI観点も含めてマニュアルテストで見ておくのがよいだろうという考えでいます。
しかし、ドラッグ&ドロップ実行後の画面のテストを自動でまわしたいというケースは、もしかしたら出てくるかもしれません。そのようなときに今回調べた方法が役に立てばと思います。3


参考サイト


  1. あくまで書き手なりの解決方法であり、ベストプラクティスの保証はありませんのでご了承ください。 

  2. このところNode.jsばかり触っていて、Rubyを書きたい衝動に駆られました。 

  3. SeleniumでのSPAのテストは面倒なことも多いので、できればそれを避けたいところではありますが。 

  4. Seleniumの公式サイトがすっかりモダンな感じにリニューアルされていてサイト内で迷子になりました。内容が空のページやサンプルコードのない箇所が散見されるのでContributeしたい……。 

NestJSで始めるGraphQLサーバ開発(コードファースト編)

$
0
0

image.png
NestJSは、TypeScriptで記述するバックエンドアプリケーションフレームワークです。デフォルトで DI(Dependency Injection) の仕組みをサポートしており、テスト可能な構成を簡単に作ることができる特徴があります。
今回の記事ではNestJSを使用して最もシンプルなGraphQLサーバを構築します。
↓完成イメージ
nestjs-graphql.gif

GraphQLの基本

GraphQLは、RESTエンドポイントのように煩雑に管理されたエンドポイントではなく、1つのエンドポイントに対して厳密に型指定されたスキーマとしてAPIを実行します。

image.png

GraphQLについて深くは解説しませんが、以下のリンクがとても参考になります。初学者は一読しておくことをオススメします。

NestJSでGraphQL

NestJSを使用したGraphQLの開発には2つの方法があります。

  • スキーマファースト
  • コードファースト

スキーマファーストのアプローチでは GraphQL SDL(スキーマ定義言語)をもとにしてTypeScript定義を自動的に生成します。
一方でコードファーストのアプローチでは、デコレータとTypeScriptのクラスのみを使用して対応する GraphQL スキーマを生成します。

今回はコードファーストのアプローチでGraphQLサーバを作成していきます。
まず始めに nestjsのコマンドラインツール@nestjs/cliをインストールしましょう。インストールができたら nest コマンドが使用できます。早速 NestJSアプリケーションを作成します。

$ npm i -g @nestjs/cli
$ nest new nest-graphql

作成されたNestJSアプリケーションを起動しましょう。

$ cd nest-graphql/
$ npm run start

> nest-graphql@0.0.1 start /Users/daisuke/work/nest-graphql
> ts-node -r tsconfig-paths/register src/main.ts

[Nest] 5868   - 2019-12-03 21:36:33   [NestFactory] Starting Nest application...
[Nest] 5868   - 2019-12-03 21:36:33   [InstanceLoader] AppModule dependencies initialized +28ms
[Nest] 5868   - 2019-12-03 21:36:33   [RoutesResolver] AppController {/}: +10ms
[Nest] 5868   - 2019-12-03 21:36:33   [RouterExplorer] Mapped {/, GET} route +16ms
[Nest] 5868   - 2019-12-03 21:36:33   [NestApplication] Nest application successfully started +6ms

ブラウザで localhost:3000 にアクセスして Hello Wold! が表示されれば準備OKです。
この状態ではまだRESTAPIの形式になっていますね。
image.png

GraphQL 関連ライブラリのインストール

GraphQLサーバを実装していきますので、まずは必要なライブラリをインストールします。

$ npm i --save @nestjs/graphql \
               apollo-server-express \
               graphql-tools \
               graphql \
               type-graphql

REST API用に作られていた app.module.ts を書き換えましょう。
Controller, Service の箇所を GraphQLModule として書き換えました。
.forRoot()メソッドで playground: trueを宣言することで ブラウザ(http://localhost:3000/graphql)で GraphQL IDEを表示できます。autoSchemaFileは自動的に生成されたスキーマが作成されるパスを示しています

app.module.ts
import{Module}from'@nestjs/common';import{GraphQLModule}from'@nestjs/graphql';@Module({imports:[GraphQLModule.forRoot({playground:true,autoSchemaFile:'schema.graphql'}),],})exportclassAppModule{}

playground はアプリケーションがバックグラウンドで実行されている間に、Webブラウザーを開いて http://localhost:3000/graphqlにアクセスすると表示できます。npm run startを実行してアプリケーションを起動してからブラウザを開いてみましょう。

$ npm run start

> nest-graphql@0.0.1 start /Users/daisuke/work/nest-graphql
> ts-node -r tsconfig-paths/register src/main.ts

[Nest] 8832   - 2019-12-03 23:44:15   [NestFactory] Starting Nest application...
[Nest] 8832   - 2019-12-03 23:44:15   [InstanceLoader] AppModule dependencies initialized +26ms
[Nest] 8832   - 2019-12-03 23:44:15   [InstanceLoader] RecipesModule dependencies initialized +1ms
[Nest] 8832   - 2019-12-03 23:44:15   [InstanceLoader] GraphQLModule dependencies initialized +0ms
[Nest] 8832   - 2019-12-03 23:44:15   [NestApplication] Nest application successfully started +82ms

image.png

Moduleを作成

NestJSの流儀に従って、まずはModuleを作成します。例としてレシピの一覧が表示できるアプリケーションを想定しています。

$ nest generate module recipes
CREATE /src/recipes/recipes.module.ts (84 bytes)
UPDATE /src/app.module.ts (325 bytes)

app.module.ts に自動的に RecipesModule が追加されるので確認しておきましょう。

import{Module}from'@nestjs/common';import{GraphQLModule}from'@nestjs/graphql';import{RecipesModule}from'./recipes/recipes.module';@Module({imports:[GraphQLModule.forRoot({playground:true,autoSchemaFile:'schema.graphql',}),RecipesModule,// <-- 自動的に追加される],})exportclassAppModule{}

Modelを作成

次に Model を作成します。
type-graphql のライブラリから各種デコレータで宣言するものを import します。

$ nest generate class recipes/recipe
CREATE /src/recipes/recipe.spec.ts (147 bytes)
CREATE /src/recipes/recipe.ts (23 bytes)
import{Field,ID,ObjectType}from'type-graphql';@ObjectType()exportclassRecipe{@Field(type=>ID)id:string;@Field()title:string;}

Resolverを作成

最後にクエリの操作を行うリゾルバを作成します。

$ nest generate resolver recipes
CREATE /src/recipes/recipes.resolver.spec.ts (477 bytes)
CREATE /src/recipes/recipes.resolver.ts (98 bytes)
UPDATE /src/recipes/recipes.module.ts (170 bytes)

このResolverに Query、Mutation、Subscriptionを実装していきます。
今回は簡単のため、データベースには接続せずにレシピの一覧を返却する処理(Query)を実装しています。

import{Resolver,Query,Args}from'@nestjs/graphql';import{Recipe}from'./recipe';constrecipeTable=[{id:'1',title:'鯖の味噌煮',},{id:'2',title:'ミートソーススパゲティ',},{id:'3',title:'豚の生姜焼',},];@Resolver('Recipes')exportclassRecipesResolver{@Query(returns=>[Recipe])asyncrecipes():Promise<Recipe[]>{returnrecipeTable;}}

ここまででディレクトリ構成は以下のようになっています。

src$ tree -L 2
.
├── app.module.ts
├── main.ts
└── recipes
    ├── recipe.spec.ts
    ├── recipe.ts
    ├── recipes.module.ts
    ├── recipes.resolver.spec.ts
    └── recipes.resolver.ts

スキーマの作成

あとはアプリケーションを起動するとスキーマが自動的に作成されます。

$ npm run start

scema.graphql にスキーマが自動的に作成されています。

#-----------------------------------------------#!!!THISFILEWASGENERATEDBYTYPE-GRAPHQL!!!#!!!DONOTMODIFYTHISFILEBYYOURSELF!!!#-----------------------------------------------typeQuery{recipes:[Recipe!]!}typeRecipe{id:ID!title:String!}

動作確認をしましょう。
http://localhost:3000/graphqlにアクセスしてクエリを実行します。
確かにフィールドごとに選択されて Query が実行できていますね。
nestjs-graphql.gif

NestJSを使用することで、モデルに対してデコレータを付与するだけでシンプルかつ簡単に実装できました。

NestJSは適切にDIをすることでコードのテスタビリティをあげることができる強力なフレームワークです。
GraphQLサーバを組む場合にも威力を発揮できる可能性があり魅力的ですね。

Discord.jsのフレームワーク「Ecstar」を作ったよって話

$
0
0

Ecstar

Discord.js のコマンド等を楽に追加するフレームワークです。

何で作ったの?

Discord、LINE、Slack の Bot などメッセージに対して反応する Bot は以下のように if 文がたくさん必要になってきます。

if(message==="aaa"){send("aaa");}if(message==="bbb"){send("bbb");}if(message==="ccc"){send("ccc");...

めんどくさいので Bot の規模が大きくなってくるとできるだけ簡潔にコマンドを増やせるように、自分で何らかの処理を書くと思います。

これでもいいのですが、複数 Bot を作成するときに同じ処理を書き直す事を考えると管理面などから大変です。

なら module 化すればよくね?それ npm に上げればよくねって事でできたのがEcstarです。

もっといい Klasa ってのがあるよって意見は受け付けません。

特徴

  1. ムダなコードが減って楽に Bot が作れる
  2. Discord.js の関数など全部利用できる
  3. TypesScript 対応(そろそろします)

使い方

現在作成中で大きな変更があると思います。
なのでこちらの最新版を確認してください。

詰まったところ

実行フォルダーの取得

実行フォルダー(Ecstar を使うフォルダー)を取得するにはprocess.argv[1]で実行したファイルまで取得できます。
そこから〇〇.jsを消すことで実行フォルダーを取得しました。

GitHub

メッセージコマンドの引数

コマンドの引数の処理にかなりの時間を費やしました。

!help all

のようなメッセージがあるときallを取得してそれに対して処理したいです。それを Ecstar 側で出力する処理をしました。

まだ結構ガバガバで納得してないので上手くできたら別で記事にしたいと思います。

EventEmitter のイベントを一括で取得したい

EventEmitter2でやった方法

通常の EventEmitter では全部を一括で取得できません。
ちょっと調べたところEventEmitter2というものがあり、これは以下のように*で全部取得できます。
emitter.on("*",functionevent(callback){console.log(`イベント名: ${this.event}返り値: ${callback}`);});

ですがEventEmitter2でイベントを受けるときアロー関数だとthis.eventでイベント名を取得できないのでなんかもっといい方法ないかなとか考えてます。

記事公開したあと考えてたらこんな感じで実装できました(別記事)
EventEmitterですべてのイベントを取得する(ワイルドカード)

npm に公開

はじめて作ったのでつまりました。
公式のドキュメントがわかりやすかったのでそれ読みつつ公開しました。

これから

  • TypeScript
  • Discordの権限別に実行できるコマンド
  • 複数のコマンド引数

終わりに

これ仮完成させたのが数ヶ月前なので思い出しつつ書きました。
今はこれを TypeScript を使ってきれいで安全なコードに書き直しています。

何か意見、質問があれば

GitHub,
Twitter,Discord コミュニティーまたはコメントまで

DIコンテナの実装を理解して、軽量 DI コンテナを自作しよう

$
0
0

なぜ DI コンテナを自作するのか

関心の分離がされているアプリケーションは変更に強く、良い設計と言えます。Dependency Injection(以下 DI) は関心の分離を実現する テクニックの 1 つとしてよく見られるパターンです。しかしクラス間の依存関係が増えれば増えるほど、注入する依存を作ることは困難になり、DI のコストは段々と膨らみます。そのようなとき、 依存を自動で解決し、欲しいインスタンスをすぐにとりだせる DI コンテナは有効な解決手段となり得ます。

JavaScript/TypeScript においても DI コンテナを提供するライブラリが存在します。例えば、InversifyJStsyringeなどが知られています。しかし既存の DI コンテナは、DI 以外の機能を持ち、また使い方も多岐にわたるため、知識の習得コストがかかります。そこで 必要最小限の機能しか持たないシンプルで軽量な DI コンテナを自作できないかと考え、実装しました。この記事ではそれを実装したときに学んだことやテクニックを紹介します。

DI 自体の説明は別の資料に纏めてありますので、不安がある方はご覧ください。

自作 DI コンテナに付ける機能

DI コンテナが備えるべき機能はどのようなものでしょうか。私は少なくとも次の機能はサポートされていて欲しいと思いました。

  • decorator ベースでの依存登録がサポートされる
  • interface に依存している場合もサポートされる
  • 1 つのクラスに inject される依存が複数ある場合もサポートされる

decorator ベース

TypeScript に限らず DI コンテナは、依存登録・依存解決の方法を設定ファイルに記述していました。しかし設定を書くことは手間だったり、実装と設定ファイルを見比べる作業が発生したりして、好まれる手順ではありませんでした。そこで依存と注入対象のコードになんらかのマーキングをして、それだけで自動的に依存関係の登録ができるような仕組みが考案されました。その実現方法として decorator が使われます。この依存登録方法は InversifyJS や tsyringe といった既存ライブラリや、Java の Spring 等でも採用・推奨されている方法です。

interface に対応

interface は TypeScript に組み込まれている標準の機能です。皆さんも interface を使って、このような型を書いた経験があると思います。

interfaceIProps{isLoading:boolean;data:IUser;error:string;}

interface は上のように型を定義できる機能ですが、クラスを抽象化したものを表現するためにも利用できます。例えば、DB もしくは API に保存する処理持つクラスは interface を使って次のように抽象化することができます。

// DBにアクセスしようがAPIにアクセスしようがそのどちらにも対応できるinterfaceIRepository{getAll:()=>IUser[];}classDBRepositoryimplementsIRepository{getAll(){// DBにアクセス}}classAPIRepositoryimplementsIRepository{getAll(){// APIにアクセス}}

このとき Repository を使いたいクラス(例えば Domain Service など)は DBRepository や APIRepository に依存するのではなく、IRepository に依存するように作り、Repository の利用側は使う IRepository に従ったクラスを実装すれば、依存を自由に入れ替えることができます。

classUserService{privatereadonlyrepository:IRepository;constrcutor(repo:IRepository){this.repository=repo;}getAllUser(){returnthis.repository.getAll();}}// DBRepository も APIRepository も同じ IRepository の実装なので、両方とも UserService に injection できるconstserviceA=newUserService(newDBRepository());constserviceB=newUserService(newAPIRepository());

しかし TypeScript においては interface はただ型検査時に使われるものでしかなく、トランスパイルすると消えます。(消える例)そのため TypeScript 製の DI コンテナは、何もしなければ interface に紐づいた詳細を見つけて DI することができません。

簡単には満たせない要求

このように DI コンテナを作ろうとすると、decorator の実装と interface への対応という 2 つの課題にぶつかります。そこで InversifyJS や tsyringe を実装した先人たちはどのようにして解決したのか、実装を読んで学んでみましょう。

DI コンテナライブラリを読み進めるために必要な知識

実装を読みましょうと言ったものの、その前に必要な知識を整理しましょう1。まず、DI コンテナは decorator によって依存が登録され、Container クラスに解決したい対象を渡すことで依存を解決、インスタンスを取得できる仕組みです。依存を登録するときには decorator と Reflection Metadata API というものを利用するため、それらについて復習しましょう。

decorator

TypeScript の decorator は、class declaration, method, accessor, property を修飾できる機能です。ここでいう修飾の定義は難しいですが、修飾されたものは、decorator 関数の中から操作することができるということだけ覚えておいてください。例えば関数の結果を URI 変換して書き換える decorator は次のように書けます。( https://qiita.com/taqm/items/4bfd26dfa1f9610128bcから例を拝借しています)

// decoratorfunctionuriEncoded(target:any,propKey:string,desc:PropertyDescriptor){constmethod=desc.value;desc.value=function(){constres=Reflect.apply(method,this,arguments);if(typeofres==="string"){returnencodeURIComponent(res);}returnres;};}classSample{@uriEncodedhoge():string{return"こんにちは";}}console.log(newSample().hoge());// 出力// %E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF

このように decorator は実行時にメソッドの実行などに割り込んで処理を挟み込むことができます。さらに decorator がとる引数は修飾対象が含まれているので、実行時に挙動を変えることが可能になります。

余談ですが decorator はそのまま定義するのではなく、decorator を返す関数などを用意して使われます。その場での設定を埋め込んだ decorator を作りたいことがあるからです。そのような関数は decorator factory と呼ばれます。

DI コンテナでは実行後にクラス decorator 経由でコンストラクタを参照し、そのコンストラクタが必要としている依存を取り出し、DI コンテナに保存します。

Reflection Metadata API

class declaration decorator を利用することで、クラスのコンストラクタにアクセスすることはできるようになります。しかし、これではまだコンストラクタが必要としている依存を取り出すことができません。

例えば下のコードの console.log で出力されるものは class そのものです。

functionclassDecorator<Textends{new(...args:any[]):{}}>(constructor:T){console.log(constructor);returnclassextendsconstructor{};}@classDecoratorclassHoge{constructor(hoge:string){}}

いま欲しいのは constructor に注入されたhogeだけです。これを抽出するためには Reflection という機能を使う必要があります。

Reflection

Reflectは JavaScript の機能です。公式の説明をそのまま引用すると「Reflect は、インターセプトが可能な JavaScript 操作に対するメソッドを提供するビルトインオブジェクト」です。この機能を使うことで、コンストラクタ の取得や実行ができます。

functionclassDecorator<Textends{new(...args:any[]):{}}>(constructor:T){console.log(Reflect.get(Hoge,"constructor"));console.log(Reflect.construct(Hoge,[]));returnclassextendsconstructor{};}@classDecoratorclassHoge{hoge:string;constructor(hoge:string){console.log("hey");}}

しかし、これでも constructor の引数の情報を引っ張ってくることはできません。そこでこの Reflect を拡張します。

reflect-metadata

reflect-metadata というライブラリを入れることで Metadata Reflection API が使えるようになります。これは TypeScript 開発チームの一部が開発に参加しているライブラリです。公式によると、次のような背景と目的を持って生まれました。(一部省略)

background

  • Decorators add the ability to augment a class and its members as the class is defined, through a declarative syntax.
  • Languages like C# (.NET), and Java support attributes or annotations that add metadata to types, along with a reflective API for reading metadata.

goals

  • A number of use cases (Composition/Dependency Injection, Runtime Type Assertions, Reflection/Mirroring, Testing) want the ability to add additional metadata to a class in a consistent manner.
  • A consistent approach is needed for various tools and libraries to be able to reason over metadata.

goals にある通り DI をサポートする機能がこの拡張で手に入ります。具体的には consturcotr からの引数取得と、interface 経由で injection するための一時的に interface と実装との紐付けの保管です。

Metadata Reflection API で何ができるようになるかは、Detailed proposalをご参照ください。その中で、DI コンテナを作るために必要になる機能は次の 3 つのみです。

getMetadata

Reflect.getMetadata(designKey, target)を呼び出すことができます。これは、target(class や function)が持つ情報を取得することができます。どのような情報を取得できるかは designKey で指定でき、それぞれ次のような情報が取得できます。

key 名取得できる情報
"design:type"引数の型
"design:paramtypes"引数の型の配列
"design:returntype"戻り値の型

どのような情報が帰ってくるかの詳しい情報は Decorators & metadata reflection in TypeScript: From Novice to Expert (Part IV)にまとまっているのでご参照ください。

defineMetadata

Reflect.defineMetadata(metadataKey, metadataValue, target)を実行します。これにより、decorator の修飾対象に、ある key に対するメタデータの組を保存できます。そしてこれは後述する getOwnMetadata を利用することでそのメタデータを取り出すことができます。自作 DI コンテナの文脈でいうと、依存している interface の具象クラスを、その interface に紐づけるために使います。

getOwnMetadata

Reflect.getOwnMetadata(metadataKey, target)によって defineMetadataで登録したメタデータを取り出すことができます。これは自作 DI コンテナの文脈でいうと、依存している interface の具象クラスがあるかどうかを調べるために使います。

既存の実装を読んでみよう 〜tsyringe を例に〜

tsyringeは Microsoft が公開した DI コンテナです。@injectableで依存を登録し、 @injectで interface への依存を注入できます。コンテナに登録された依存関係からインスタンスを取り出すためには container.resolve(${class name})を実行します。基本的にはこの 3 つだけ覚えておけばよく設定ファイルも不要なため2、手軽です。実際のところ、これ以外の機能はほぼないため学習コストも低くシンプルで、DI コンテナを 3 つほど試したことがある私からしても一番好みです。まずはこの tsyringe を読むことで、DI コンテナはどのように実装されているかをみていきましょう。

クラス間の依存を登録(injectable)

injectable.tsにこの injectable decorator factory があります。

functioninjectable<T>():(target:constructor<T>)=>void{returnfunction(target:constructor<T>):void{typeInfo.set(target,getParamInfo(target));};}

ここでは なんらかの store に constructor を key にした、parameterInfo を保存していることが分かります。
この parameterInfo を作成している getParamInfo の実装をみてみましょう。

exportfunctiongetParamInfo(target:constructor<any>):any[]{constparams:any[]=Reflect.getMetadata("design:paramtypes",target)||[];constinjectionTokens:Dictionary<InjectionToken<any>>=Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY,target)||{};Object.keys(injectionTokens).forEach(key=>{params[+key]=injectionTokens[key];});returnparams;}

Reflect.getMetadata("design:paramtypes", target)を使えば、constructor に紐づいている変数名や型情報をオブジェクトとして取得できます。ここでは constructor が要求している依存の情報を取得し、返却しています。

間に挟まっているコードは、interface に依存している場合に使う機能です。詳しくは次の節で紹介します。

interface への依存を登録(inject)

inject.tsにこの decorator factory があります。

functioninject(token:InjectionToken<any>):(target:any,propertyKey:string|symbol,parameterIndex:number)=>any{returndefineInjectionTokenMetadata(token);}exportfunctiondefineInjectionTokenMetadata(data:any):(target:any,propertyKey:string|symbol,parameterIndex:number)=>any{returnfunction(target:any,_propertyKey:string|symbol,parameterIndex:number):any{constinjectionTokens=Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY,target)||{};injectionTokens[parameterIndex]=data;Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY,injectionTokens,target);};}

inject が必要になる背景

先ほどの injectable が interface を実装したクラスである場合、実際にコンストラクタに注入された依存の具象は何かはわかりません。最初の Repository の例で言えば、Repository を使う Domain Service はどの永続化レイヤーに依存するかは知りません。

// DBにアクセスしようがAPIにアクセスしようがそのどちらにも対応できるinterfaceIRepository{getAll:()=>IUser[];}classDBRepositoryimplementsIRepository{getAll(){// DBにアクセス}}classAPIRepositoryimplementsIRepository{getAll(){// APIにアクセス}}@injectable()classUserService{privatereadonlyrepository:IRepository;constrcutor(repo:IRepository){this.repository=repo;}getAllUser(){returnthis.repository.getAll();}}constserviceA=newUserService(newDBRepository());constserviceA=newUserService(newAPIRepository());

UserService に @injectable() decorator があることに注意してください。このとき Service についた @injectable()の中で Reflection Metadata API を用いて constructor の情報をみたとき、serviceA, serviceB において双方とも IRepository という情報しか手に入りません。そのため実際に注入された依存は何かを明示的に伝える必要があります。tsyringe ではその機能を @inject()として提供しており、constructor の引数で利用します。

@injectable()classUserService{privatereadonlyrepository:IRepository;constrcutor(@inject("IDBRepository")repo:IRepository){this.repository=repo;}getAllUser(){returnthis.repository.getAll();}}

このとき @inject()の引数は、他の inject の引数と衝突しなければなんでもいいですが、依存の名前などにしておくとよいでしょう。衝突を避けるために Enum を定義したり、Symbol を利用することもあります。

inject はなにをしてくれているのか

injectable に次のコードがあったことを思い出してください。

constinjectionTokens:Dictionary<InjectionToken<any>>=Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY,target)||{};Object.keys(injectionTokens).forEach(key=>{params[+key]=injectionTokens[key];});constinjectionTokens:Dictionary<InjectionToken<any>>=Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY,target)||{};Object.keys(injectionTokens).forEach(key=>{params[+key]=injectionTokens[key];});

const injectionTokens: Dictionary<InjectionToken<any>> = Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};ここでは登録したい依存に紐づく何かがないかを探しています。Reflect Metadata API では修飾対象の情報を取得するだけでなく、metadata を保存する Map(?)的なものも提供しています。

ここでは仮に依存が interface だった場合にその実装が何かを探しに行っています。

Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY,injectionTokens,target);

つまり、この機能を呼ぶことで、interface 越しにも依存が解決できるようになるわけです。

依存を解決(resolve)

一番読み応えのある機能でした。

コンテナ

DI コンテナはコンテナとよばれる物の中に依存を登録し、そこから依存を解決していきインスタンスを生成してくれます。そのコンテナ自体は class として定義されています。

classInternalDependencyContainerimplementsDependencyContainer{// 300行くらい続く}

依存の解決

@injectableで登録した Map は {constructor: [dependency, ...], ...}といった組を持っています。container に生えている resolve(arg)メソッドは渡された引数の constructor を Map にある依存関係を参照しながらインスタンス化していきます。

Map をみると key にある constructor をインスタンス化するためには dependency を引数に入れてインスタンス化する必要があります。ただし、dependency をインスタンス化するためには、その constructor で再度 Map を検索し、インスタンス化可能かどうかを確認する必要があります。そのためこの resolve メソッドは、依存解決を再帰的に実行します。

publicresolve<T>(token:InjectionToken<T>,context:ResolutionContext=newResolutionContext()):T{constregistration=this.getRegistration(token);// - 中略 -returnthis.construct(tokenasconstructor<T>,context);}privateconstruct<T>(ctor:constructor<T>,context:ResolutionContext):T{// - 中略 -constparamInfo=typeInfo.get(ctor);// - 中略 -constparams=paramInfo.map(param=>{// - 中略returnthis.resolve(param,context);});returnnewctor(...params);}

ここで注意したいことは context と呼ばれるものです。これは ResolutionContext という名前の型が付いており、その名の通り依存解決の途中結果を保存するための Map です。これを掘っていくと {[Provider]: any}という組の Map であることがわかりますが、ほとんどのユースケースでは{[constructor]: any}という組になるでしょう。

これは何を表しているかといえば、さらに読み進めていくと、依存解決時に対象の constructor をインスタンス化したときの組を保存していることがわかります。

privateresolveRegistration<T>(registration:Registration,context:ResolutionContext):T{// - 中略 -if(registration.options.lifecycle===Lifecycle.ResolutionScoped){context.scopedResolutions.set(registration,resolved);}returnresolved;}

自作軽量 DI コンテナに挑戦しよう

ここまでで tsyringe で DI するときに何がされているかを読みすすめました。コードをみて気づかれたかもしれませんが、かなり中略しており、実際にはさまざまな処理がたくさんあります。また tsyringe には class constuctor 以外の依存の解決、複数の container の利用、双方向の依存に対処するなどといったユースケースも想定されており、ただ DI をしたいというニーズに対しては機能過多な部分があります。機能過多だと、ただ使いたいだけというニーズによっては学習コストがかかり障壁ともなり得り、ソースコードリーディングの際にも読みにくいポイントが生まれたりもします。そこで decorator ベースで DI をする最小構成を作ってみましょう。

依存を保存できるコンテナを作る

まず依存を登録できるコンテナを作ります。
複数コンテナでの運用は考えないので、シングルトンで作ります。

classContainer{privatestaticinstance:Container;data:Map<constructor<any>,constructor<any>[]>;context:Map<constructor<any>,constructor<any>>;privateconstructor(){this.data=newMap<constructor<any>,constructor<any>[]>();this.context=newMap<any,constructor<any>>();}staticgetInstance(){if(!Container.instance){Container.instance=newContainer();}returnContainer.instance;}}exportdefaultContainer;

constructor という型は別の場所で、次のように定義します。

exporttypeconstructor<T>={new(...args:any[]):T;};

そして DI コンテナは依存を登録できるので、登録するための関数をコンテナに生やします。

classContainer{// -中略-publicregister(constructor:constructor<any>,depends:constructor<any>[]){this.data.set(constructor,depends);}}

依存を登録する機能を作る

interface を経由しない場合

次に依存を登録する機能を作ります。
これは tsyringe に倣って @injectable()という名の Class Decorator を定義しましょう。

exportconstinjectable=():ClassDecorator=>{returntarget=>{constparams:any[]=Reflect.getMetadata("design:paramtypes",target)||[];Container.getInstance().register(target,params);};};

Reflect.getMetadata("design:paramtypes", target)で decorator で修飾された class の constrcutor の引数の constructor を取得します。そしてそれを、Container.getInstance().register(target, params);で DI コンテナに保存します。

interface を経由させる場合

これも tsyringe に倣って @inject(${class名})という Class Decorator を定義しましょう。

この decorator は DI コンテナに登録される依存を上書きするものです。ユースケースとしては@injectable()経由で登録された依存情報が interface のものだったときです。これを@inject(${class名})で具象クラスの constructor にすり替えるようにしたいです。

まず、@inject()は次のように定義します。

exportconstinject=(token:InjectionToken<any>):((target:any,propertyKey:string|symbol,parameterIndex:number)=>any)=>{returndefineInjectionTokenMetadata(token);};constdefineInjectionTokenMetadata=(data:any):((target:any)=>any)=>{returnfunction(target:any):any{constinterfaceName:any={};interfaceName[0]=data;Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY,interfaceName,target);};};

この実装は tsyringe とほとんど同じものです。
@inject()の定義に propertyKey,parameterIndex と言ったものが出てきますが、これは使いません。
しかし@inject()は decorator factory なので decorator が取りうる引数をとる関数を返さないといけません。そのためこの不要な引数は省略することができません。

decorator factory である@inject()が返す decorator では、次のことがされています。

Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY,interfaceName,target);

これによりINJECTION_TOKEN_METADATA_KEYというキーで interfaceName と target が紐づいていることを引っ張ってこれるようになりました。

そしてすり替えてコンテナに保存できるように @injectableを拡張します。

exportconstinjectable=():ClassDecorator=>{returntarget=>{constparams:any[]=Reflect.getMetadata("design:paramtypes",target)||[];// NEWconstinjectionTokens:Dictionary<InjectionToken<any>>=Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY,target)||{};Object.keys(injectionTokens).forEach(key=>{params[+key]=injectionTokens[key];});// NEWContainer.getInstance().register(target,params);};};

これで interface 越しに依存を登録できるようになりました。

依存を解決する機能を作る

それでは登録した依存を解決し、インスタンスを取り出す機能を作りましょう。

依存を解決する関数を作る

依存を解決する関数として resolveを作りましょう。

publicresolve(ctor:constructor<any>){// 受け取った依存を注入するために依存をインスタンス化する関数を呼び出す(できないときもある)this.resolveInstance(ctor);// resolveしたいクラスの依存を取得するconstdependantClasses=this.data.get(ctor);// その依存のインスタンスを全て取得する(この時点で全依存はインスタンスされている想定)if(!dependantClasses)return;constinstances=dependantClasses.map(cls=>this.context.get(cls));// 依存を全て注入してインスタンス化returnnewctor(...instances)}publicresolve(ctor:constructor<any>){// 注入しなければいけない依存のコンストラクタを取得consttargetDependencies=this.data.get(ctor);if(targetDependencies&&targetDependencies.length>0){// 注入しなければいけない依存がなければ即時インスタンス化constinstance=newctor();this.context.set(ctor,instance);}// 注入しなければいけない依存をインスタンス化する//(引数が注入される側なのはI/Fとしてはいけてないです。すみません。)this.resolveInstance(ctor);constdependantClasses=this.data.get(ctor);// 必要な解決済み依存を取得constinstances=dependantClasses.map(cls=>this.context.get(cls));// 依存を全て注入してインスタンス化returnnewctor(...instances);}

依存の解決とは、インスタンス化を指します。
しかし、依存を解決しようとするも、その依存をインスタンス化しようとして、別の依存がある場合もあります。
そのため、依存解決を再帰的に行う仕組みを作ります

privateresolveInstance(ctor:any){// 引数のコンストラクタをインスタンス化するために必要な依存を取得constdepends=this.data.get(ctor);if(!depends){// もし必要な依存がないなら、そのままコンストラクタをインスタンス化するconsti=newctor();this.context.set(ctor,i);return;}// 必要な依存あるなら、そのままinstance化できるまで resolveを再帰的に呼ぶ// 依存が一方向であることを前提にしているのでこう書いても最終的に依存を解決できる// (単独でインスタンス化できるコンストラクタにいつか出会えるから)this.resolve(depends[0]);// 必要な依存の全インスタンスを取得constdependInstances=depends.map(d=>{returnthis.context.get(d);});// その依存を注入し保存するconstinstance=newctor(...dependInstances);this.context.set(ctor,instance);}

思ったよりも resolve をシュッと書けたのではないでしょうか。実は双方向の依存をサポートする機能を入れていないので、このように単純にすることができました。クリーンアーキテクチャの本などでは依存の方向を一方向にするように書かれており、自分はそのような設計しかしないのでサポートをしませんでした。そのおかげで DI コンテナの設計をかなり削ることができました。

自作 DI コンテナはちゃんと動くのか

こちらが自作したDIコンテナです。ここにある example を実行します。

依存の階層が多い場合

// so many nest exampleimport{injectable}from"../main/Injectable";import"reflect-metadata";importContainerfrom"../main/Container";classA{call(){console.log("CALL A");}}@injectable()classB{a:A;constructor(a:A){this.a=a;}}@injectable()classC{b:B;constructor(b:B){this.b=b;}}@injectable()classD{c:C;constructor(c:C){this.c=c;}}@injectable()classE{d:D;constructor(d:D){this.d=d;}}constcontainer=Container.getInstance();conste=container.resolve(E);e.d.c.b.a.call();

これを実行すると・・・

$ node dist/example/test1.js
CALL A

インターフェースに依存する場合

// can revolve via interfaceimportContainerfrom"../main/Container";import{injectable}from"../main/Injectable";import"reflect-metadata";import{inject}from"../main/inject";interfaceIRepository{read:()=>number[];create:(val:number)=>void;}interfaceIStoreAdapter{read:()=>number[];// in real, should return a DTO.create:(val:number)=>void;}classMemoryStoreImplimplementsIStoreAdapter{privatestore:number[]=[];read(){returnthis.store;}create(val:number){this.store.push(val);}}@injectable()classDBRepositoryImplimplementsIRepository{adapter:IStoreAdapter;constructor(@inject(MemoryStoreImpl)adapter:IStoreAdapter){this.adapter=adapter;}read(){returnthis.adapter.read();}create(val:number){this.adapter.create(val);}}@injectable()classAPIRepositoryImplimplementsIRepository{read(){console.log("get data");return[1,2,3];}create(val:number){console.log("post data");}}@injectable()classService{repo:IRepository;constructor(@inject(DBRepositoryImpl)repo:IRepository){this.repo=repo;}publicfind(){returnthis.repo.read();}publicsave(val:number){this.repo.create(val);}}// interface testconstcontainer=Container.getInstance();constservice=container.resolve(Service);service.save(1);service.save(2);constdata=service.find();console.log("the value: ",data);

これを実行すると・・・

$ node dist/example/test2.js
the value:  [ 1, 2 ]

複数の依存を受け取る場合

import{injectable}from"../main/Injectable";import"reflect-metadata";importContainerfrom"../main/Container";classHoge{call(){console.log("hogeeeeeeeeeeee");}}classPiyo{call(){console.log("piyooooooooooooo");}}@injectable()classFuga{hoge:Hoge;piyo:Piyo;constructor(hoge:Hoge,piyo:Piyo){this.hoge=hoge;this.piyo=piyo;}}@injectable()classFoo{fuga:Fuga;constructor(fuga:Fuga){this.fuga=fuga;}}constcontainer=Container.getInstance();constfoo=container.resolve(Foo);foo.fuga.hoge.call();foo.fuga.piyo.call();
$ node dist/example/test2.js
hogeeeeeeeeeeee
piyooooooooooooo

まとめ

いかがでしたか。自作 DI コンテナは仕組みさえわかれば以外と簡単に作れます。
それに tsyringe に比べるとかなりシンプルにすることができました。
しかし実際に運用されるコードや規模の大きいコードを書こうとすると、「テストを書きやすくするために、設定ファイルによってあとから依存を差し替えたい」「いちいちコンテナを作って resolve せずに、自動で解決したものをインスタンス化して取り出したい」といったニーズが出てくるでしょう。
残念ながら自作 DI コンテナにはその機能はないですし、恐らくそのような機能を生やしていくと tsyringe に近づいていていくと思います。
DI コンテナはあまり多様性がなく、色々な機能が足されていっているものだと思います。
その中でも tsyringe は必要最小限の機能を全て盛り込んだ最小の DI コンテナだと思っており、お勧めします。


  1. 今はデコレータベースのものを読むこととし、PROXY ベースのものは扱いません。 

  2. test のときだけ依存をモックに変えるようなことをしようとすると設定ファイルが出てくる 

Paiza Cloud で LINE Bot を試してみる

$
0
0

プロトアウトスタジオアドベントカレンダー4発目の記事です!

昨日は @tkyko13さんの「word2vecの勉強で「word2vecの勉強で「ナダルリバースエボリューション」が再現できるのではないかと思いついたのでやってみた」でした。

Paiza Cloud とは

クラウド開発環境 PaizaCloudクラウドIDE - クラウドIDEでWeb開発!

2019-12-03_01h00_54.png

ブラウザを開くだけでLinuxサーバが使える!
クラウド開発環境PaizaCloudクラウドIDEでは、ブラウザだけでLinuxサーバを操作できます。ファイル操作、テキスト操作、コマンド操作、Webサーバ/DBサーバの立ち上げなど、全てブラウザだけで行えます。 もう、面倒なコマンドでのログイン(ssh)やファイル操作(vim)、ファイルのアップロードは必要ありません。 目の前のコンピュータと同じように、クラウド上のLinuxサーバを操作できます。

とのことで、以前、Katacodaで LINE Messaging API Playground (ja)を作った身としては興味があります。

image.png

料金表をみてみても1つのサーバーが無料で24時間使えるので、Katacodaよりも長時間使えます。(2019/12/03現在)

サーバーを立ち上げる

Paiza Cloud のアカウント登録をして、まず、Node.jsサーバを立てあげてみましょう。

image.png

新規サーバーを押します。

image.png

Node.jsをクリックして新規サーバ作成します。

image.png

このような形で起動します。

LINE Bot をつくる

1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest - Qiita

こちらの資料をベースに、「1. Botアカウントを作成する」を進めてBotと友達になるところまで進めましょう。

「2. Node.jsでBot開発」からはじめます。

image.png

左のメニューのターミナルを押して、ターミナルを起動します。

npm i @line/bot-sdk express

を実行します。

image.png

無事インストールされました。

image.png

新規ファイルを押して、

image.png

server.js ファイルを作成します。

以下をコピー&ペーストしましょう。

server.js
'use strict';constexpress=require('express');constline=require('@line/bot-sdk');constPORT=process.env.PORT||3000;constconfig={channelSecret:'作成したBOTのチャンネルシークレット',channelAccessToken:'作成したBOTのチャンネルアクセストークン'};constapp=express();app.get('/',(req,res)=>res.send('Hello LINE BOT!(GET)'));//ブラウザ確認用(無くても問題ない)app.post('/webhook',line.middleware(config),(req,res)=>{console.log(req.body.events);//ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。if(req.body.events[0].replyToken==='00000000000000000000000000000000'&&req.body.events[1].replyToken==='ffffffffffffffffffffffffffffffff'){res.send('Hello LINE BOT!(POST)');console.log('疎通確認用');return;}Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));});constclient=newline.Client(config);functionhandleEvent(event){if(event.type!=='message'||event.message.type!=='text'){returnPromise.resolve(null);}returnclient.replyMessage(event.replyToken,{type:'text',text:event.message.text//実際に返信の言葉を入れる箇所});}app.listen(PORT);console.log(`Server running at ${PORT}`);

image.png

保存します。

自分のBotとして動くように、Channel SecretとChannel Access Tokenを反映

自分のBotとして動くように、Channel SecretとChannel Access Tokenを反映させます。

1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest - Qiita

の流れに沿ってChannel SecretとChannel Access Tokenをメモしておきます。

左上のファイルツリーから server.js を選択してエディタで編集できるようにします。

constconfig={channelSecret:'channelSecret',channelAccessToken:'channelAccessToken'};

server.jsのこちらを変更します。仮に Channel Secret が ABCDEFGHIJ 、channelAccessTokenが 1234567890 とすると。

constconfig={channelSecret:'ABCDEFGHIJ',channelAccessToken:'1234567890'};

と、なります。

ファイルを保存します。

server.jsを動作させてWebhook URLを設定

ターミネルウィンドウで以下のコマンドを打ち込んで起動します。

node server.js

起動すると Server running at port 3000 と表示されたらOKです。

image.png

公開URL確認

image.png

起動すると左のメニューに 3000 というボタンが出来るので、公開URLを確認します。

image.png

内部ブラウザが開いてURLが確認できるのでメモしておきましょう。

image.png

こちらをLINEの管理画面のメッセージ送受信設定>Webhook URLに反映します。

これで準備完了です。

動かしてみる

実際にLINEでBotを会話してオウム返しを体験しましょう。

image.png

LINEで話しかけてみると無事返答されます。

image.png

ターミナルを見てみると

image.png

サーバーのやり取りも確認できます。

明日の記事は…

@doikatsuyukiさんの「Firebaseを利用した中耳炎診療支援Webアプリの作成 (1.Firebaseの設定~認証方法の追加)」です!

初心者にMongoDBを教えようと自作パッケージを作って奮闘した話

$
0
0

ごあいさつ

初投稿です。よろしくお願いします。
駒場祭という学園祭でプログラミングをしたりしてました。基本的にNode.jsを使っています。

注意

この記事はあくまでやったことの紹介であり、解決策は提示していません。

この記事は......

駒場祭委員会にはシステム局というIT分野を担当する部署があり、ウェブサイトや、参加される企画の登録をしたり、申請や情報を集めたりするウェブシステムと呼ばれるシステムなどを例年作っています。
無給のブラック学生自治団体であるため、経験者は余り集まらず、キャンパスが変わるため2年生までしか参加しません。
よって初心者の1年生に2年生が引退するまでの半年間で様々な知識を教え込むことになります。

駒場祭のウェブサイトには(現在はもう使えませんが)当日に公式グッズの売り上げやキャンパスツアー企画の参加賞配布状況
が分かるページもあり、開発ではフロントエンド(見た目の部分)だけでなく、サーバーで動くバックエンドも重要になっています。

この記事では特にバックエンドに関して、初心者に伝えようとした僕の奮闘の記録です。
一番悩んで工夫したつもりになっているMongoDBに関して特に扱います。

技術的にも内容的にも面白くは無いと思いますが、学園祭プログラマーの方や、「非IT企業でIT部門のメンバーがコロコロ変わってしまい技術引き継ぎが難しい...」などといった悩みをお抱えの方に役立てばと思います。

そもそも何が難しいのか

バックエンドとは何か

初心者にとって「フロントエンド」「バックエンド」という言葉は余り聞き慣れないものでしょう。
「みんなの使ってるブラウザがフロントエンドだよ。バックエンドってのはサーバーで動いてるもので、例えばログインデータはサーバーにないと改竄されちゃうよね。」
的な感じで説明しました。

DBについて

「データベースってものが色々あってデータを同時に読み書きしたり、検索したりするのに便利だから使うんだ。駒場祭で使うデータはサーバーに入れておくんだ。」
といった感じで説明しました。僕自身1年前にデータベースについて聞いた際に必要性がわからなかったので(当時の僕「え?jsonファイルで保存すればいいじゃんw」)、何が便利なのかを説明しました。またMySQL?DB?ってなっていたので、データベースの種類としてMySQLや今回扱うMongoDBがあるということも説明しました。

MongoDBのわかりにくさ

MongoDBをNode.jsで扱う際、基本的にはmongodbという公式のパッケージを使います。
mongooseなどのパッケージもありますが、さらに複雑になるため初心者向けではないということで以下では無視します。)
これは扱うのがなかなか面倒で、データを持ってくるために

constmongodb=require('mongodb');constMongoClient=mongodb.MongoClient;constclient=awaitMongoClient.connect('mongodb://127.0.0.1:27017/',{useUnifiedTopology:true,useNewUrlParser:true,});constdb=awaitclient.db('dbName');constcollection=db.collection('collectionName');constdata=awaitcollection.find().toArray();client.close();

と書く必要があります。

これはawaitを使っているのでまだマシですが、async/awaitを封印するとより複雑になってしまいます。
よってasync/awaitなしで教えるのは難しく、とりあえずasync/awaitを教えて.......となってしまいます。非同期を教えた話は長くなるので省略します。

もう1つ、「接続して、dbを選択して、collectionを選択して、......」となると複雑であり、「そもそもDBってなんや」ってなってた人は混乱しちゃいます。

さらにMongoDBについて学ぼうとすると、Node.js以外の話なども出てきて「Node.js」があまりわかってない初心者には「ggってもよくわからない......」となってしまいます。

どうすればいいのでしょうか......?

自作パッケージで解決だ!

さて上記の問題を解決しないといつまでたっても1年生がDBを使いこなせません。そこで「ggるのが難しいならggれなくていいじゃないか!」「とりあえず単純にして慣れてもらおう!」ということでパッケージを作ってしまいました。

それがmongodbeginnerGitHub)です。
基本的に内部で教える際に使うために作ったので色々と雑で実用に耐えるものかは怪しいです。

使い方

mongodbeginnerでは初心者が とりあえず DBを使えるように工夫した結果、接続などの処理を毎回行います。
例えば id1のデータをfindしたい際には

mongodbeginnerのサンプル
constmob=require('mongodbeginner')constdata=awaitmob.find("dbName","collectionName",{id:1})

とすればOkです。
接続して......ってのが複雑なのが解決したのではないでしょうか?
もし {id: 1, count: 0}というデータをinsertしたい場合は

mongodbeginnerのサンプル2
constmob=require('mongodbeginner')constdata=awaitmob.insert("dbName","collectionName",{id:1,count:0})

などとします。

接続して...ってとこが複雑すぎるという問題は解決したのではないでしょうか?

問題点

これをやったのが8月なのですでに直したいところも多々ありますが、そもそもの実力不足もありなんとも言えない出来になってしまいました。
というかもし自信があったら制作時点でQiitaにドヤ顔で紹介記事を書いていました。

最大の問題は結局awaitが必要になってしまうことでしょう。
これの解決策は特に思いついてなく、結局やはりPromiseやasync/awaitを初心者にも覚えてもらうしかないのでしょうか......?。

超簡易版 npmにパッケージを公開する方法

なかなかnpmにパッケージを公開するのは思った以上に簡単でした。特にこちらの記事を参考にしました。

内部向けパッケージだったのでテストもなく簡単でした。せっかくなのでいつものNode.jsアプリケーションの開発と違う部分をメモしておきます。

npmへのuser登録
$ npm set init.author.name "名前"$ npm set init.author.email "メールアドレス"$ npm set init.author.url "URL"$ npm adduser
初回公開
$ git tag -a v1.0.0 -m"My first version v1.0.0"$ git push origin tags/v1.0.0
$ npm publish ./
パッチアップデート(修正)
$ npm version patch
$ git push origin tags/v1.0.1  # ここは手動でやるしかない$ npm publish ./
マイナーアップデート
$ npm version minor
$ git push origin tags/v1.1.0  # ここは手動でやるしかない$ npm publish ./
メジャーアップデート
$ npm version major
$ git push origin tags/v2.0.0  # ここは手動でやるしかない$ npm publish ./

まとめ

とりあえずMongoDBを1年生に慣れてもらうためにいろいろとやろうとしました。
ただ結果としては初心者にはやはり理解し難かったのかと思います。

また自分の経験からDBを難しいものとばかり考えて、駒場祭で使用したWebアプリケーション・フレームワークのExpress.jsの解説を軽視してしまった結果、そっちで詰まっていた様子も感じたので、やはり初心者にバックエンドプログラミングを教えるというのは難しいものでした。

......ruby on railsでもやろうかなぁ。時代遅れって聞くこともあるけど...

Microsoft Custom Vision Serviceによる中耳炎画像認識LINE Botの作成

$
0
0

概要

プログラムの勉強を始めて5か月ほどの開業医です。

前回、Microsoft Custom Vision Service を使用して鼓膜画像認識を試し、極めて高い診断精度でした。
Microsoft Custom Vision Service を使用した鼓膜画像認識

前回は「正常鼓膜」か、「急性中耳炎」か、「滲出性中耳炎」かを分けるためのタグだけでしたが、今回は急性中耳炎の重症度を判定できるようにするため「鼓膜の発赤の程度」、「鼓膜の腫脹の程度」、「耳漏の有無」に関するタグに追加しました。

さらに、LINE Botと連携しNowでデプロイしました。

実装

スマホから鼓膜の写真をLINE Bot宛てに送ると、中耳炎かどうか応えてくれるLINE Bot。

概念図

image.png

動作確認

作成方法

1.タグの付けなおし

以下のようにタグを付けなおしていきます。
正常鼓膜は「正常鼓膜」「発赤:なし」「腫脹:なし」「耳漏:なし」のタグを、
滲出性中耳炎は「滲出性中耳炎」「発赤:なし」「腫脹:なし」「耳漏:なし」のタグを、
急性中耳炎は「急性中耳炎」そして鼓膜発赤の程度により「発赤:なし」「発赤:一部」「発赤:全体」のタグを、鼓膜腫脹の程度により「腫脹:なし」「腫脹:一部」「腫脹:全体」のタグを、耳漏の有無により「耳漏:なし」「耳漏:あり」のタグを付けました。

2.再トレーニング

タグが多くなったためか全体の精度が落ちました。

image.png

正確な判定のためにはタグ毎に最低30枚の画像が必要なようですが、一部30枚未満のタグができてしまいました。

image.png

3.テスト

テストデータ30枚をテストします。
「正常鼓膜」か「急性中耳炎」か「滲出性中耳炎」かの診断は前回同様100%正解しました。
急性中耳炎の重症度判定に使用する「発赤の程度」「腫脹の程度」は間違っているところがありました。

発赤全体 ➡ 一部 が正解
image.png
発赤全体 ➡ 一部 が正解
image.png

正解
image.png
正解
image.png
正解
image.png
正解
image.png
腫脹一部 ➡ 全体 が正解
image.png
正解
image.png

4.LINE Bot との連携

Azure Custom Vision ServicesのPerformanceからPublishをクリックし、Prediction APIを発行します。

「If you have an image file:」のURLと
「Set Prediction-Key Header to :」のKeyを後で使うのでひかえておきます。

5.LINE BoTの作成

こちらの記事を参考にしました。
はやい!やすい!うまい!Custom Vision と LINE bot でお寿司の判定をしてみた
LINE動物図鑑の作り方

このようなコードを書きました。

'use strict';constexpress=require('express');constline=require('@line/bot-sdk');constPORT=process.env.PORT||3000;constfs=require('fs');constbodyParser=require('body-parser');constRequest=require('request');constcv=require('customvision-api');constconfig={channelSecret:'自分のchannelSecret',channelAccessToken:'自分のchannelAccessToken'};constapp=express();app.use(bodyParser.json());letmiddle=line.middleware(config);constclient=newline.Client(config);app.post('/webhook',(req,res)=>{console.log(req.body.events);if(req.body.events[0].message.type!=='image')return;// ユーザーがLINE Bot宛てに送った写真のURLを取得するconstoptions={url:`https://api.line.me/v2/bot/message/${req.body.events[0].message.id}/content`,method:'get',headers:{'Authorization':'Bearer 自分のchannelAccessToken',},encoding:null};Request(options,function(error,response,body){if(!error&&response.statusCode==200){//保存console.log(options.url+'/image.jpg');letstrURL=options.url+'/image.jpg';//Nowでデプロイする場合は、/tmp/のパスが重要fs.writeFileSync(`/tmp/`+req.body.events[0].message.id+`.png`,newBuffer(body),'binary');constfilePath=`/tmp/`+req.body.events[0].message.id+`.png`;//Azure Custom Vision APIの設定constconfig={"predictionEndpoint":"ひかえておいたURL","predictionKey":'ひかえておいたKey'};cv.sendImage(filePath,config,(data)=>{console.log(data);letProbability0=data.predictions[0].probability*100;letProbability1=data.predictions[1].probability*100;letProbability2=data.predictions[2].probability*100;letProbability3=data.predictions[3].probability*100;letProbability4=data.predictions[4].probability*100;letstrName0=data.predictions[0].tagName;letstrProbability0=Probability0.toFixed();letstrName1=data.predictions[1].tagName;letstrProbability1=Probability1.toFixed();letstrName2=data.predictions[2].tagName;letstrProbability2=Probability2.toFixed();letstrName3=data.predictions[3].tagName;letstrProbability3=Probability3.toFixed();letstrName4=data.predictions[4].tagName;letstrProbability4=Probability4.toFixed();client.replyMessage(req.body.events[0].replyToken,{type:'text',text:strName0+':'+strProbability0+'%,\n'+strName1+':'+strProbability1+'%,\n'+strName2+':'+strProbability2+'%,\n'+strName3+':'+strProbability3+'%\n'+strName4+':'+strProbability4+'%'//実際に返信の言葉を入れる箇所});try{fs.unlinkSync(filePath);returntrue;}catch(err){returnfalse;}return;},(error)=>{console.log(error)});}else{console.log('imageget-err');}});});(process.env.NOW_REGION)?module.exports=app:app.listen(PORT);console.log(`Server running at ${PORT}`);

6.Nowでデプロイ

こちらの記事を参考にしました。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest

考察

結構簡単にAIによる画像認識モデルとLINE BoTを連携できました。
今後は重症度判定に必要なタグを含んだ急性中耳炎の画像を増やし、精度を上げていきたいと思います。
そして以前作った中耳炎診療ガイドラインに沿った診断や治療選択ができるBOT
急性中耳炎診断支援LINE Botを改良しHerokuにデプロイ
に組み込んで、質問に返答し鼓膜の画像を送れば、自動で診断や治療方針が決定させるBOTを作成したいと思います。


EventEmitterですべてのイベントを取得する(ワイルドカード)

$
0
0

Node.jsのEventEmitterは便利なんですがすべてのイベントを取得できません。
何で実装していないのかよくわかりません。

EventEmitter2というEventEmitterを便利にしたものがありこれを使えば良いのですが、更新が止まっています...

頑張ればできるんじゃねと思って書いたら数分でできたのメモ的な意味を込めて記事にしてます。

コード

index.js
// いつものEventEmitterconstEventEmitter=require("events");// いつものEventEmitterを拡張classExtendEventEmitterextendsEventEmitter{// emitされた内容を"*"に再emitemit(name,...args){returnsuper.emit("*",name,...args);}}// 拡張したEventEmitterconstevent=newExtendEventEmitter();// ワイルドカードでイベントを受けるevent.on("*",(name,...callback)=>{console.log(`name: ${name} |`,...callback);});/* emit */event.emit("ready","ready...");event.emit("number",1,2,3,4);event.emit("array",["a","b"]);event.emit("object",{"abc":123,"def":456});

結果

name:ready|ready...name:number|1234name:array|['a','b']name:object|{abc:123,def:456}

Node.jsのHTTPリクエストヘッダの最大サイズでハマった話

$
0
0

現象

  • Node.js(v12.3.1)で立てたWebサーバにアクセスすると、時折HTTPリクエストに失敗する
  • Cookieを削除したり、ブラウザを再起動すると治ることもあるが、根本的な原因がわからない
サンプルコード
consthttp=require('http');constserver=http.createServer((req,res)=>{res.writeHead(200,{'Content-Type':'text/plain'});res.end('Hello World');});server.listen(8080);

原因

  • Node.jsの最大HTTPリクエストヘッダサイズのデフォルト値である8kBを越えるHTTPリクエストヘッダサイズを送信していたことが原因だった

  • Node.jsは、2018/11にDoS攻撃の脆弱性対応として、デフォルトのHTTPリクエストヘッダの最大サイズを変更前の80kBから8kB(8192Bytes)に変更する修正が加えられた

  • デフォルトでは、HTTPリクエストヘッダのサイズが8kBを越えるとソケットが強制破棄されて「431 Request Header Fields Too Large」を返す

$ npm start

> sample-nodejs-header-overflow@1.0.0 start /../../../sample-nodejs-header-overflow
> node index.js

// curlで8kB以上のHTTPリクエストを送信
ErrorCode:  HPE_HEADER_OVERFLOW
BytesParsed:  8559

// curlで8kB以上のHTTPリクエストを送信
ErrorCode:  HPE_HEADER_OVERFLOW
BytesParsed:  8559

// curlで8kB以上のHTTPリクエストを送信
ErrorCode:  HPE_HEADER_OVERFLOW
BytesParsed:  9085

対策

  • HTTPリクエストヘッダのサイズが超えた場合に起こるclientErrorイベントを補足して、ソケットが強制的に破棄されないようにエラーハンドリングを行う
consthttp=require('http');constserver=http.createServer((req,res)=>{res.writeHead(200,{'Content-Type':'text/plain'});res.end('Hello World');});server.on('clientError',(err,socket)=>{console.log('ErrorCode: ',err.code);console.log('BytesParsed: ',err.bytesParsed);socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');});server.listen(8080);
  • アプリケーション起動時に「--max-http-header-size」という起動オプションを設定して、Node.jsが受け取る最大のHTTPリクエストヘッダサイズを増やす
$ node --max-http-header-size=16384 index.js

おわりに

今回リクエストヘッダのサイズが8kBを越えた主な原因は、多数のCookieを使ってWebサーバにアクセスしていたことでした。
仕様上Cookieの数が多くなり、HTTPリクエストのサイズに不安がある場合は、エラーハンドリングを正しく実装して、起動オプションでNode.jsが受け取るHTTPリクエストサイズの最大値を上げておくと良いかと思います。

参考

NestJS でダミーの Service を注入し、外部依存のないテストを実行する

$
0
0

この記事は NestJS アドベントカレンダー 4 日目の記事です。

はじめに

先日は Module と DI について説明しましたが、本日はもう一歩進んだ DI を活用したテストを実施してみます。
なお、サンプルでは MySQL に接続したり Docker を使用したりしていますが、怖がらないでください。
この記事では MySQL や Docker に依存せずにテストできるようにするテクニックを説明します。

サンプルコードのリポジトリは以下になります。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day4-inject-dummy-service-to-avoid-external-dependency

なお、環境は執筆時点での Node.js の LTS である v12.13.1 を前提とします。

サンプルアプリの雛形を作る

今回のサンプルとなるアプリケーションの雛形を cli を用いて作ってゆきます。

$ nest new day4-inject-dummy-service
$ nest g module items
$ nest g controller items
$ nest g service items

ItemsController には以下のように Post と Get を実装していきます。

import{Controller,Post,Body,Get}from'@nestjs/common';import{CreateItemDTO}from'./items.dto';import{ItemsService}from'./items.service';@Controller('items')exportclassItemsController{constructor(privatereadonlyitemsService:ItemsService){}@Post()asynccreateItem(@Body(){title,body,deletePassword}:CreateItemDTO){constitem=awaitthis.itemsService.createItem(title,body,deletePassword,);returnitem;}@Get()asyncgetItems(){constitems=awaitthis.itemsService.getItems();returnitems;}}

ItemsService も雛形を作成します。

@Injectable()asynccreateItem(title:string,body:string,deletePassword:string){return;}asyncgetItems(){return[];}}

MySQL にデータを書き込む箇所を実装する

今回は Service の外部依存先として、 MySQL を例にあげます。
MySQL に接続するため、以下のライブラリをインストールします。

$ yarn add typeorm mysql

なお、今回は TypeORM の複雑な機能は極力使用せずにサンプルを記述します。
TypeORM についての説明や NestJS との組み合わせ方については別の記事で説明します。
また、本来は constructor で非同期の初期化を行うべきではないのですが、回避策は複雑なので、こちらも別途説明します。

@Injectable()exportclassItemsService{connection:Connection;constructor(){createConnection({type:'mysql',host:'0.0.0.0',port:3306,username:'root',database:'test',}).then(connection=>{this.connection=connection;}).catch(e=>{throwe;});}// connection が確率していないタイミングがあるため待ち受けるprivateasyncwaitToConnect(){if(this.connection){return;}awaitnewPromise(resolve=>setTimeout(resolve,1000));awaitthis.waitToConnect();}asynccreateItem(title:string,body:string,deletePassword:string){if(!this.connection){awaitthis.waitToConnect();}awaitthis.connection.query(`INSERT INTO items (title, body, deletePassword) VALUE (?, ?, ?)`,[title,body,deletePassword],);}asyncgetItems(){if(!this.connection){awaitthis.waitToConnect();}constrawItems=awaitthis.connection.query('SELECT * FROM items');constitems=rawItems.map(rawItem=>{constitem={...rawItem};deleteitem.deletePassword;returnitem;});returnitems;}}

また、 MySQL を Docker で立ち上げます。

$ docker-compose up

Docker ではない MySQL で実行する場合、 MySQL に testデータベースを作り、 create-table.sqlを流してください。

この状態でアプリケーションを起動してみましょう。MySQL が起動していれば、無事起動するはずです。

$ yarn start:dev

続いて curl でアプリケーションの動作確認をしてみます。

$ curl -XPOST-H'Content-Type:Application/json'-d'{"title": "hoge", "body": "fuga", "deletePassword": "piyo"}' localhost:3000/items
$ curl locaohost:3000/items
[{"title":"hoge","body":"fuga"}]

無事保存できるアプリケーションができました。

MySQL がない状態でもテストできるようにする

アプリケーションができたので、Mock を使ってテストを記述します。

前回までのサンプルでは特に DI を意識する必要がなかったため new ItemsService()としてテストを記述していましたが、
今回は DI に関連するため、 cli で自動生成される雛形にも用いられている Testモジュールを使用します。

describe('ItemsController',()=>{letitemsController:ItemsController;letitemsService:ItemsService;beforeEach(async()=>{consttestingModule:TestingModule=awaitTest.createTestingModule({imports:[ItemsModule],}).compile();itemsService=testingModule.get<ItemsService>(ItemsService);itemsController=newItemsController(itemsService);});describe('/items',()=>{it('should return items',async()=>{expect(awaititemsController.getItems()).toHaveLength(1);});});});

さて、この状態でテストを実行するとどうなるでしょうか。
MySQL を起動している場合はそのままテストが通りますが、 MySQL を停止すると以下のようにテストが落ちてしまいます。

$ jest
 PASS  src/app.controller.spec.ts
 FAIL  src/items/items.controller.spec.ts
  ● ItemsController › /items › should return items

    connect ECONNREFUSED 0.0.0.0:3306

          --------------------
      at Protocol.Object.<anonymous>.Protocol._enqueue (../node_modules/mysql/lib/protocol/Protocol.js:144:48)
      at Protocol.handshake (../node_modules/mysql/lib/protocol/Protocol.js:51:23)
      at PoolConnection.connect (../node_modules/mysql/lib/Connection.js:119:18)
      at Pool.Object.<anonymous>.Pool.getConnection (../node_modules/mysql/lib/Pool.js:48:16)
      at driver/mysql/MysqlDriver.ts:869:18
      at MysqlDriver.Object.<anonymous>.MysqlDriver.createPool (driver/mysql/MysqlDriver.ts:866:16)
      at MysqlDriver.<anonymous> (driver/mysql/MysqlDriver.ts:337:36)
      at step (../node_modules/tslib/tslib.js:136:27)
      at Object.next (../node_modules/tslib/tslib.js:117:57)

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        1.204s, estimated 3s

ItemsService を Mock していますが、 ItemsService の初期化自体はされており、初期化処理の中で MySQL への接続しようとしているのが原因です。
このような、 外部へ依存する Provider の初期化をテストから除外するために、 ItemsService を上書きした状態で testingModuleを生成する機能が NestJS には備わっています。

以下のように DummyItemsService class を定義し、 overrideProviderを使って上書きします。

classDummyItemsService{asynccreateItem(title:string,body:string,deletePassword:string){return;}asyncgetItems(){constitem={id:1,title:'Dummy Title',body:'Dummy Body',};return[item];}}describe('ItemsController',()=>{letitemsController:ItemsController;letitemsService:ItemsService;beforeEach(async()=>{constapp:TestingModule=awaitTest.createTestingModule({imports:[ItemsModule],}).overrideProvider(ItemsService).useClass(DummyItemsService).compile();itemsService=app.get<ItemsService>(ItemsService);itemsController=newItemsController(itemsService);});describe('/items',()=>{it('should return items',async()=>{expect(awaititemsController.getItems()).toHaveLength(1);});});});

useClass()の代わりに useValue()を使うことで、 class ではなく変数で上書きすることもできます。

この状態でテストを実行すると、 MySQL が起動していなくても問題なく通過します。

yarn run v1.19.0
$ jest
 PASS  src/items/items.controller.spec.ts
 PASS  src/app.controller.spec.ts

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.406s
Ran all test suites.
✨  Done in 2.94s.

おわりに

この記事で NestJS の持つ強力な DI の機能をお伝えできたかと思います。
より詳細な内容は公式のドキュメントの E2E テストの項にあるので、合わせてご確認ください。
https://docs.nestjs.com/fundamentals/testing#end-to-end-testing

また、今回説明できなかった TypeORM との合わせ方や、非同期の初期化を必要とする Service の扱い方については、後日別の記事で説明します。

明日は @potato4dさんが ExceptionFilter についてお話する予定です。

【待望リリース!】もう Lambda×RDS は怖くない!LambdaでRDSプロキシを徹底的に検証してみた 〜全てがサーバレスになる〜

$
0
0

本日の reinvent でのリリースで衝撃のアップデートがたくさん出ましたね。EKS on Fargate や SageMaker の大幅アップデートも魅力的ですが Lambda の常識をくつがえす RDS のプロキシ機能が登場しました 🎉

Lambda から RDS に対するアクセスはコネクション数の上限に達してしまうという理由からアンチパターンとされてきました。そのため、RDS をデータストアに選択する場合は ECS や EC2 上にアプリケーションをホストする事が一般的でした。Lambda の接続先 DB に RDS を選べるということはほとんどのWebアプリケーションがサーバレスで実行できる夢が広がります。

本記事では RDS プロキシを使った Lambda の構成を作ってコネクション数の挙動について検証してみました。
https://aws.amazon.com/blogs/compute/using-amazon-rds-proxy-with-aws-lambda/
※本記事は上記のブログを参考にしています。一部文脈で引用している箇所があります。

image

RDS プロキシは、データベースへの接続プールを維持します。これにより Lambda から RDS データベースへの多数の接続を管理できます。
Lambda 関数は、データベースインスタンスの代わりに RDS プロキシと通信します。スケーリング起動した Lambda 関数によって作成された多くの同時接続をスケーリングするために必要な接続プーリングを処理します。これにより、Lambda アプリケーションは関数呼び出しごとに新しい接続を作成するのではなく、既存の接続を再利用できます。

従来はアイドル接続のクリーンアップと接続プールの管理を処理するコードを用意していたのではないでしょうか。これが不要になります。劇的な進化です。関数コードは、より簡潔でシンプルで、保守が容易になります。

現在はまだプレビュー版ですがこの機能を徹底検証していきましょう。

せっかく検証するのですから従来 ECS などで一般的に使ってたフレームワークを例に上げてみましょう。今回は NestJS を Lambda にデプロイして RDS と接続してみます。

lamba-rds-proxy.png

NestJS を Lambda にデプロイする

Serverless Frameworkを使用して NestJS アプリケーションを AWS Lambda にデプロイします。
こちらのサンプルソースが参考になりました。ほぼそのまま引用させていただきます。

Lambda がデプロイできたことを確認しておきましょう。

$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
~~~~~~~~~~~~~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~~~~
endpoints:
  ANY - https://djpjh5aklf.execute-api.us-east-1.amazonaws.com/dev/
  ANY - https://djpjh5aklf.execute-api.us-east-1.amazonaws.com/dev/{proxy+}
functions:
  index: serverless-nestjs-dev-index
layers:
  None
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.

生成されたエンドポイントにアクセスします。

image.png

これで準備ができました。まずは Hello World!と文字列を返す NestJS アプリケーションを Lambda をデプロイできました。これから MySQL と接続できるアプリケーションを作っていきます。開発過程は省略しますが、以下のリポジトリに完成品をアップロードしておきます。

完成品:https://github.com/daisuke-awaji/serverless-nestjs

参考:Nest(TypeScript)で遊んでみる 〜DB 連携編〜

タスクの CRUD 操作ができるアプリケーションを用意しました。
image.png

Secret Manger に RDS への接続情報を登録

事前に作成しておいたこちらの RDS を使用します。
image.png

まずは Secret Manger コンソールで RDS への接続情報を登録するようです。
image.png
image.png

シークレットができたら ARN をメモしておきましょう。あとで使います。

IAM

次に、RDS プロキシがこのシークレットを読み取ることができる IAM ロールを作成します。RDS プロキシはこのシークレットを使用して、データベースへの接続プールを維持します。IAM コンソールに移動して、新しいロールを作成します。 前の手順で作成したシークレットに secretsmanager アクセス許可を提供するポリシーを追加します 。

IAM ポリシー

image.png

image.png

IAM ロール

image.png
image.png

rds-get-secret-role という名前で IAM ロールを作成しました。

RDS Proxy

さて、ここからが本題です。
RDS のコンソールを開くと Proxies の項目があります。Lambda の接続先をこのプロキシに向けることでコネクションプールをうまく使いまわしてくれるようです。

image.png

作成してみましょう。先ほど作成した IAM ロールや RDS を入力します。
image.png
image.png
image.png

作成まではしばらく時間がかかるようです。
image.png

Lambda の向き先を RDS から RDS Proxy に切り替える

RDS インスタンスに対して直接接続する代わりに、RDS プロキシに接続します。これを行うには、2 つのセキュリティオプションがあります。IAM 認証を使用するか、Secrets Manager に保存されているネイティブのデータベース認証情報を使用できます。IAM 認証は、機能コードに認証情報を埋め込む必要がないため、推奨されているようです。この記事では、Secrets Manager で以前に作成したデータベース資格情報を使用します。

DBに接続するアプリケーションの設定を変更してデプロイしましょう。

db.config.ts
import{TypeOrmModuleOptions}from"@nestjs/typeorm";import{TaskEntity}from"./tasks/entities/task.entity";exportconstdbConfig:TypeOrmModuleOptions={type:"mysql",host:"rds-proxy.proxy-ch39q0fyjmuq.us-east-1.rds.amazonaws.com",// <-- DBの向き先をProxyに切り替えるport:3306,username:"user",password:"password",database:"test_db",entities:[TaskEntity],synchronize:false};
$ npm run build && sls deploy

まだ Serverless では RDS Proxy をサポートしていないようでしたので Lambda のコンソールから設定してみます。セキュリティグループやサブネットなどは適宜各自の環境に合わせて作成してください。

image.png
image.png

RDS Proxy 経由でも無事に接続できました 🎉
※事前に DB にはテストデータを入れてあります
image.png

準備に使用した SQL

CREATETABLE`tasks`(`id`int(36)unsignedNOTNULLAUTO_INCREMENT,`overview`varchar(256)DEFAULTNULL,`priority`int(11)DEFAULTNULL,`deadline`dateDEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=94000001DEFAULTCHARSET=utf8mb4;INSERTINTO`tasks`(`id`,`overview`,`priority`,`deadline`)VALUES(1,'掃除',0,'2020-11-11'),(2,'洗濯',2,'2020-12-03'),(3,'買い物',0,'2020-11-28');

負荷テストを実行してみる

コネクション数が Lambda のスケールに合わせて増え続けるような挙動を取らないか確認してみましょう。

今回は負荷のために Artillaryを使用します。
yaml ファイルでシナリオを記述して実行する Nodejs 製の負荷テストツールです。

Artillary のインストール

$ npm install -g artillery

実行

yaml ファイルを記述しなくてもワンラインで実行できる手軽さも魅力的なツールで愛用しています。
以下のようなコマンドで簡単に実行できます。30 ユーザが 300 回リクエストを送るといった内容です。

$ artillery quick --count 300 -n 30 https://djpjh5aklf.execute-api.us-east-1.amazonaws.com/dev/tasks

実行された Lambda を確認します。Invocations が 9000 回を記録しています。

image.png

一方で RDS のコネクション数はなんと 43 になっていました。すごい。
ちなみに MySQL の現在のコネクション数は show status like 'Threads_connected'で確認できます。

負荷テスト開始前最大リクエスト時
1843

RDS Proxy を使わない場合はどうなるか

アプリケーションの向き先を RDS 本体に直接接続するように変更してみます。
この状態でもう一度負荷テストを行うとどうなるでしょうか。

import{TypeOrmModuleOptions}from'@nestjs/typeorm';import{TaskEntity}from'./tasks/entities/task.entity';exportconstdbConfig:TypeOrmModuleOptions={type:'mysql',host:'aurora.cluster-ch39q0fyjmuq.us-east-1.rds.amazonaws.com',// <-- RDS 本体に向けるport:3306,username:'user',password:'password',database:'test_db',entities:[TaskEntity],synchronize:false,};

実行

コネクション数が 124 まで膨れ上がってしまいました。
やはりプロダクションロードで普通に Lambda+RDS の組み合わせはやってはいけないアンチパターンになりそうですね。RDS Proxy の威力を改めて感じることができました。

$ artillery quick --count 300 -n 10 https://djpjh5aklf.execute-api.us-east-1.amazonaws.com/dev/tasks
負荷テスト開始前最大リクエスト時
18124

まとめ

RDS プロキシを使用することで、データベースへの接続プールを保持することが確認できました。これで API やユーザリクエストを受けるようなワークロードでも Lambda から RDS への多数の接続を管理できます。とてつもなく強力なアップデートを体感できました。今後追加で RDS Proxy を使用する場合と使用しない場合とで、レスポンスタイムに違いが出てくるのかなど細かなところまで検証したいと思います。

クラウドはいよいよここまで成長してきました。
次は RDS がインスタンスを意識することなく水平にスケールするようになるのでしょうか。
その時は完全にサーバレスなクラウドが完成しますね。待ち遠しいです。

Node.jsのasync/awaitとPromiseを超ざっくり

$
0
0

現在関わっているプロジェクトでNode.jsを使って開発しています。
その過程でハマったこと、今回はasync/await・Promiseについての記事になります。

といってもNode.jsを使ったことのない人もいると思うので簡単にNodeの非同期処理について紹介してから、ハマったポイントについて書きたいと思います。
※100番煎じなので非同期の書き方に関してはあんまり詳しく書きません。
※arrow関数使ってません。説明省くので。
※間違いあれば教えていただければと思います。

目次

  1. Node.jsの非同期処理について
  2. 非同期処理の書き方
    2.1 コールバック(callback)関数
    2.2 Promise   ←この記事はココまで!
    2.3 asycn/await
    3. Promiseとasycn/awaitが一緒だと思ったらハマった件
  3. 参考

Nodeを使ったことがあって1~2についてわかってるよって方は3まで飛ばしてください。

1.Node.jsの非同期処理について

Nodeは非同期を多用するという特徴がある。

非同期処理とは

よそに処理を依頼したときに、その場で完了を待たない

処理のことらしい。
ここでは詳しい説明は割愛するが、Nodeは単一のスレッドしか持たないため(シングルスレッドアーキテクチャ)、いちいち同期処理をしていては他の処理の実行を妨げてしまう。

例)同期処理の場合

example_sync.js
//受け取った数字を出力する(同期)functionprintNumSync(num){console.log(num)}printNumSync(1)printNumSync(2)printNumSync(3)

結果

console
1
2
3

同期処理の場合は呼び出した順番通りに処理が実行されます。
→前の処理の完了を待ってから次の処理に進む。

・非同期処理の場合
次に同期、非同期、同期処理の順に呼び出してみます。

example_async.js
//受け取った数字を出力する(同期)functionprintNumSync(num){console.log(num)}//受け取った数字を1秒後に出力する(非同期)functionprintNumAsync(num){setTimeout(function(){console.log(num)},1000);}printNumSync(1)printNumAsync(2)printNumSync(3)

結果

console
1
3
2

先に述べたように非同期処理(例の場合はsetTimeoutが非同期)は、完了を待たずに次の処理へ進むため1の出力後、2つ目の処理の完了前に3が表示されます。

2.非同期処理の書き方

非同期処理にもいくつか書き方があります。

2.1.コールバック関数

「コールバック関数」とはわかりやすく言えば 終わったらコレやっといてコレに当たる部分。全然わかりやすくないですね。はい。
非同期処理には処理の完了を待たないという性質がありました。では非同期処理が終わったら次に何をすればいいのか。そんな悩みにこたえるのがコールバック関数。
非同期処理にコールバック関数を渡す1ことで、非同期処理の完了後にコールバック関数が実行される。

実は上の非同期の例でも使われていますがsetTimeoutを見てみましょう。

setTimeout
//1秒後に「callbackの中です」と表示setTimeout(function(){console.log('callbackの中です')},1000);//↓↓わかりやすくするとこうなる//callbackFuncに関数を格納varcallbackFunc=function(){console.log('callbackの中です')}//「setTimeout」が終わったら「callbackFunc」をやっといて//→ ≒「1秒待つ」が終わったら「callbackFunc」をやっといてsetTimeout(callbackFunc,1000);

2.2.Promise

以下Node.jsデザインパターンより引用

プロミスは簡単に言えば、「非同期処理の結果を表現するオブジェクト」です。~中略~
プロミスは完了される(fulfilled、成功)か棄却される(rejected、失敗)のいずれかで、このいずれかが起きることは保証されます。完了されてから後で棄却されたり、複数の結果が起きることはありません。

ようは非同期処理が成功したか、失敗したかを扱うオブジェクト。
例)使い方

example_promise.js
varfs=require('fs').promises;//ライブラリ読み込み//1.ファイル読み込み(Promiseを返却する)fs.readFile('./work.txt').then(function(content){//2.ファイル読み込みが完了したら実行されるconsole.log(content.toString())}).catch(function(error){//2.ファイル読み込みが失敗したら実行されるconsole.log(error)});

時間になってしまったので続きは後日書きます。

4.参考

Node.js 非同期処理・超入門 -- Promiseとasync/await
Node.jsデザインパターン


  1. Node(というかECMAScript)では関数はオブジェクトとみなされるため、変数に入れることができます。 

Viewing all 9047 articles
Browse latest View live