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

[Node.js] Handlebars Tips

$
0
0

概要

テンプレートエンジンであるHandlebarsをいろいろ触ってみた。
Handlebars自体は珍しいライブラリではなく、他に記事があるので、日本語情報が見つからなかった機能を主に紹介する。

https://handlebarsjs.com/
Handlebarsを使う際は、ここを一度は目を通すことをお勧めする。

ここでは、通常のテンプレートとしての機能は紹介しない。
環境は、AWS Lambda Node.js上で動かす事を想定している。

pertialとhelper

Handlebarsで機能を拡張する方法には、この2種類がある。

  • partial : いわゆるサブテンプレート、テンプレートに動的に他のテンプレートを埋め込む事ができる。
  • helper : 関数。テンプレートの中でヘルパ関数を実行し、その結果をtemplateに埋め込む事ができる。
{{!sample.hbs}}{{#ifhoge}}{{sample.sub.hbshoge}}{{/if}}

例えばこんな感じで、sample.hbsというテンプレートに、ある条件に合致したときのみsample.sub.hbsという別のテンプレートを埋め込む。

{{!sample.hbs}}{{>hogea"aaa"}}{{/if}}
// helperes.jsconsthoge=(key1,key2)=>{returnkey+':'+value;};Handlebars.registerHelper('hoge',hoge);

たとえばこんなことをすると、aの値と":aaa"文字列を結合した結果を出力してくれる。
こんな単純なHelper関数だけではなく、{{#if}}のようなブロックヘルパー(内部に構造を持つヘルパー)を作成することもできる。

Tips紹介

precompile

コードの中でreadFileしてコンパイルすることも可能だが、簡単にprecompileできる。

templateの場合

{{!test.hbs}}{{a}}

たとえばあらかじめこんなテンプレートを作成しておき、

$ handlebars test.hbs -f test.hbs.js

コマンドを実行することで、jsファイルにコンパイルされる。

global.Handlebars=require('handlebars')require('test.hbs')consttemplate=Handlebars.templates['test.hbs']console.log(template({a:1}))// 1

プリコンパイルしたコードを読み込めば、templateを実行することができる。

  • 前もってglobalにHandlebarsを設定する必要がある。

    この情報がどこにも書かれていない。サンプルを見ると、precompileしたコードをfileとして読んで、Handlebars.templateでロードしろと言っているようにも見えるが・・・そんなことをするよりこっちのほうが簡単で便利なきがするのだが・・・(もちろん名前空間の問題を抜きにすればだが)

  • テンプレートは、Handlebars.templatesにファイル名をキーに登録される。

partialの場合

partialもtemplateと同様にプリコンパイルできる。

{{!test.hbs}}{{test.sub.hbsa}}{{! partialにはコンテキストを渡すことができる。}}
{{!test.sub.hbs}}{{b}}

たとえばあらかじめこんなテンプレート、サブテンプレートを作成しておき、

$ handlebars test.hbs -f test.hbs.js
$ handlebars test.sub.hbs -p-f test.sub.hbs.js

コマンドを実行することで、jsファイルにコンパイルされる。

global.Handlebars=require('handlebars')require('test.hbs')require('test.sub.hbs')consttemplate=Handlebars.templates['test.hbs']console.log(template({a:{b:1}}))// 1
  • partialの扱いもtemplateとほぼ同じ。プリコンパイルオプションに-pを設定するだけ。
  • partialタグがインデントされていた場合、中身も自動的にインデントされる。

global.Handlebarsについて

globalにHandlebarsをセットする方法は推奨ではない可能性もある。
とすると以下のようにする必要がある。

constHandlebars=require('handlebars');Handlebars.partials['sample.hbs']=Handlebars.template(fs.readFileSync('sample.hbs.js'));

ただ、せっかくprecompileしたコードをわざわざreadFileで読むのか?それとももっといい方法があるのか?

globalに設定する場合、通常はpreloadすることになるだろう。

// setup.jsglobal.Handlebars=require('handlebars')
// package.json{"jest":{"verbose":true,"setupFilesAfterEnv":["<rootDir>/setup.js"]}}

jestでは、これでpreloadされた。

$ node -r ./setup.js

言わずもがなだと思うが、nodeであればこれでpreloadできる。

Helperについて

helperから@rootを取得したい

helperからコンテキストの情報や@rootが欲しくなった場合、どうすればいいだろうか?

// helpers.jsconstgetRoot=(key,_)=>{return_.data.root[key];};Handlebars.registerHelper('getRoot',getRoot);
{{!test.hbs}}{{#witha}}{{b}}{{getRoot"c"}}{{/with}}

ヘルパー関数の通常の引数の後ろに、handlebarsの環境情報が全て格納されているからここから取得できる。

この情報も見つける事ができなかった。APIドキュメントにも記載はなかった。
重要な情報だと思うのだが・・・

ちなみに、現在のコンテキストだけならもっと簡単に取れる。

// helpers.jsconstgetValue=function(key){returnthis[key];};Handlebars.registerHelper('getValue',getValue);

ラムダ式ではなくfunctionで関数定義すれば、thisでコンテキストを参照できる。

helperの一括登録

// helpers.jsconsthelper1=()=>{...}consthelper2=()=>{...}Handlebars.registerHelper({helper1,helper2});

複数のHelper関数を一括で登録できる。

Helperのテスト

helperに対して単体テストを書く場合、

// helpers.test.js// must preload global.Handlebarsrequire('helpers.js')const_=Handlebars.compile;describe('test helpers',()=>{test('getRoot',()=>{consttemplate=_('{{#with a}}{{b}}{{getRoot "c"}}{{/with}}');expect(template({a:{b:1},c:2})).toEqual('12');});});

こんな感じで、小さなテンプレートを使ってテストを書いていけばいい。
巨大なproductionテンプレートを使ってhelperのテストを書こうとすると、膨大な量のテストになるだろう。
単体テストをしよう。

結論

とりあえず上記により、Handlebarsを使ってやりたかった事はできました。
ただし、正直これが正しい作法かどうかはわかりません。ご意見があれば下さい。

もっと言えば、もうtemplateはReactあたりで書いて、jsx ==> jsにプリコンパイルすればいいんじゃないかという気もしなくもありません。


esbuildがwebpackより187倍早いらしいので環境構築しよう

$
0
0

はじめに

久しぶりの投稿になります。
今回は以下の記事で、esbuidがすごい!!という話を聞きつけこの記事を書くことにしました。
参考: [Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れない

どのくらいすごいのでしょうか?
参考に挙げている記事によると

esbuild は Go 言語で書かれた JavaScript および TypeScript のビルドツールです。 esbuild 単体でトランスパイル + バンドル + ミニファイできます。 JSX / TSX もサポートされています。そしてめっちゃくちゃ速いという触れ込みです。最初から速度を意識して無駄がないように書かれており、構文解析・出力・ソースマップ生成は並列化され、ネイティブコードで動作します。公式の README では three.js のビルドが Rollup + terser より 100 倍速い と謳っています。
https://www.kabuku.co.jp/developers/ultrafast-tsx-build-tool-esbuild

とのことです。
なるほどなるほどと。最近わたしもWebpackのBuild遅いなあと思っていたのでこの情報を鵜呑みにして、esbuildの環境構築をしたくなりました。

また、ドキュメントによるとesbuildはwebpackの187倍もBuildが早いみたいです。

esbuildの環境構築

環境構築についてですが、だいたいのモジュールバンドラにはコンフィグファイルがつきものです。esbuildのgithubを参考にしましたが、ワンライナーのCL上での実行例のみでコンフィグファイルらしいものの書き方見当たりません。
少し詳細に調べたところ、
https://github.com/evanw/esbuild/issues/39
以下に書いてありました。
ふむふむ、一応、対応はしているみたいです。

前準備

今回は、React+TypeScriptで記述されたプロジェクトを対象にBuildを行う環境構築をします。Vueについては、viteというモジュールバンドラがesbuildを利用して、Buildを行っているため、そちらを利用してくださいとのことです。

まずは以下の通りにnpmモジュールをインストールします。
npm install -D esbuild @types/node

Reactのプロジェクトは後ほど説明する注意点に気をつければなんでも大丈夫です。
わたしのgithubのプロジェクトを例に説明します。
https://github.com/olt556/esbuild-tmp

Buildスクリプトの作成

まず最低限、以下の条件を実現したいです。

  • developmentとproductionの環境でBuildを分ける
  • エントリーポイントの指定
  • Build後に出力されるESの規格の指定
  • プラットフォームの指定(node, browser)
  • production時にはminifyをかける
  • 出力先ディレクトリの指定
  • tsconfig.jsonの読み込み

以上の条件を元に作成した、esbuildのBuildファイルは以下の通りです。

build.ts
const{argv}=require('process')const{build}=require('esbuild')constpath=require('path')// optionsの定義constoptions={// 以下のdefineプロパティを設定しない場合Reactのプロジェクトの実行時にエラーが出ますdefine:{'process.env.NODE_ENV':process.env.NODE_ENV},entryPoints:[path.resolve(__dirname,'src/Index.tsx')],minify:argv[2]==='production',bundle:true,target:'es2016',platform:'browser',outdir:path.resolve(__dirname,'dist'),tsconfig:path.resolve(__dirname,'tsconfig.json')}// Buildの実行build(options).catch(err=>{process.stderr.write(err.stderr)process.exit(1)})

上記のファイルを以下のnodeコマンドで実行することでBuild可能です。
node build.ts production/development

npmスクリプトにして実行しやすくするのもいいかもしれませんね。

実装時の注意点(2020/08/24)

現時点でesbuildを利用する際には、以下のような注意点があります。

  • TypeScriptのトランスパイル時の型チェックに対応していません
  • css-modulesに対応していません
  • pluginsに対応していないので、各種loaderの読み込みや詳細な設定をすることはできません

おわりに

webpackesbuildに置き換えることによって、Reactを導入しているプロジェクトがCSSフレームワークや、CSS in JSを利用している場合、かなりの効果を発揮するかもしれませんね!!

もし何か質問やご指摘などありましたらお願いします!

Expressとpassportで簡単にOpenID ConnectのRPを作成してみた

$
0
0

目的と前提

認証/認可について少しづつですが備忘録としてまとめようと思います。
今回は、Nodejsを使ったRPの作成[1]です。
OpenID Connectのアクセストークン取得まで実装しています。
(UserInfoを取得するところは実装していません)

IdPの作成にはオープンソースソフトのOpenAM[2]を使用しています。

認証/認可、基礎的なOpenID Connectの知識があることを前提としています。

環境

macOS Catalina v10.15.5
OpenAM 14.5.1 Build d8b8db3cac (2020-March-11 23:25)
node v13.13.0

利用モジュール
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"morgan": "~1.9.1",
"passport-openidconnect": "0.0.2"
}

OpenAMの起動と初期設定

IdPはDockerで提供されているOpenAMを利用して作成します。
OpenAMのイメージはDockerHubよりゲットできます。

$docker pull openidentityplatform/openam
$docker run -h openam-01.domain.com -p 8080:8080 --name openam-01 openidentityplatform/openam

これでOpenAMが起動したはずです。

念のため、起動しているか確認してみます。
下記のような表示が出れば、問題なく起動できています。

$docker container lsCONTAINER ID        IMAGE                         COMMAND                  CREATED             STATUS              PORTS                    NAMES
91d60b3e3538        openidentityplatform/openam   "/usr/local/tomcat/b…"   2 hours ago         Up 2 hours          0.0.0.0:8080->8080/tcp   openam-01

それではOpenAMにアクセスしてみましょう。
http://localhost:8080/openam

初回起動では設定事項がいろいろあるので、私は、OpenAMコンソーシアムの資料を参考に設定しました。
※設定オプションは、カスタム設定ではなく、デフォルト設定を選択しました。
https://www.openam.jp/wp-content/uploads/techtips_vol1.pdf

IdPの作成

まずOpenAMにamAdminでログインします。
スクリーンショット 2020-08-24 18.51.20.png
その後、Top Level Realm(トップレベルレルム)にアクセス後、Configure OAuth Providerを選択します。

スクリーンショット 2020-08-24 19.14.17.png

Configure OpenID Connectを選択します。
スクリーンショット 2020-08-24 19.15.36.png
認可コードやアクセストークンの有効期限をチェックして、問題なければ作成を押します。
リフレッシュトークンを発行させたい場合は、リフレッシュトークンの発行にチェックを入れてください。
スクリーンショット 2020-08-24 19.19.30.png

これでIdPが作成できました〜
下記URLにアクセスしてIdPができていることを確認します。
http://localhost:8080/openam/oauth2/.well-known/openid-configuration

{"response_types_supported":["code token id_token","code","code id_token","id_token","code token","token","token id_token"],"claims_parameter_supported":false,"end_session_endpoint":"http://localhost:8080/openam/oauth2/connect/endSession","version":"3.0","check_session_iframe":"http://localhost:8080/openam/oauth2/connect/checkSession","scopes_supported":["address","phone","openid","profile","email"],"issuer":"http://localhost:8080/openam/oauth2","id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"acr_values_supported":[],"authorization_endpoint":"http://localhost:8080/openam/oauth2/authorize","userinfo_endpoint":"http://localhost:8080/openam/oauth2/userinfo","device_authorization_endpoint":"http://localhost:8080/openam/oauth2/device/code","claims_supported":["zoneinfo","address","profile","name","phone_number","given_name","locale","family_name","email"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","A128KW","RSA1_5","A256KW","dir","A192KW"],"jwks_uri":"http://localhost:8080/openam/oauth2/connect/jwk_uri","subject_types_supported":["public"],"id_token_signing_alg_values_supported":["ES384","HS256","HS512","ES256","RS256","HS384","ES512"],"registration_endpoint":"http://localhost:8080/openam/oauth2/connect/register","token_endpoint_auth_methods_supported":["client_secret_post","private_key_jwt","client_secret_basic"],"token_endpoint":"http://localhost:8080/openam/oauth2/access_token"}

こんな風に表示されればOK!

RPの作成

初期設定

express公式サイトのGetting startedに従って、まずサンプルのWebアプリケーションを作成します。

$ mkdir myapp
$ npx express-generator

こんな感じのディレクトリ構成になっているはず

$ ls
app.js                  node_modules            package.json            routes
bin                     package-lock.json       public                  views

実装

実装はForgeRockのOpenIDConnectのサンプルRP[3]を参考にしながら作成していきます。(非常に分かり易かったのでオススメ!)

SSO連携というリンクをクリックすると、
OpenIDConnectのフローが開始されるようにしていきます。
見た目はこんな感じ
スクリーンショット 2020-08-24 20.08.01.png

viewsにリンクのボタンを加えます。

views/index.jade
extends layout

block content
  h1= title
  p Welcome to #{title}

  hoge-button
    a(href="http://localhost:3000/auth/openidconnect", target="_blank") SSO連携

routes/index.js
varexpress=require("express");varrouter=express.Router();/* GET home page. */router.get("/",function(req,res,next){res.render("index",{title:"Express"});});module.exports=router;

コントローラー(app.js)にロジックを直接追加しちゃいます。
気になる方は分けていただいても問題ないです。

app.js
// 参考:https://github.com/ForgeRock/exampleOAuth2Clients/tree/master/node-passport-openidconnect// 各モジュールをインポートvarcreateError=require("http-errors");varexpress=require("express");varpath=require("path");// sessionを使うのに求められるvarcookieParser=require("cookie-parser");varlogger=require("morgan");// pathを定義// indexにログインボタンを設置// ログイン失敗時 → loginfail// ログイン成功時 → login// に遷移するようにするvarindexRouter=require("./routes/index");varloginFRouter=require("./routes/loginfail");varloginRouter=require("./routes/login");varapp=express();//session有効varsession=require("express-session");app.use(session({//クッキー改ざん検証用IDsecret:"YOUR_PASSWORD",//未初期化のセッションを保存するかsaveUninitialized:false,//他にもsessionの寿命とか、httpsならsecureも設定できる}));// view engine setupapp.set("views",path.join(__dirname,"views"));app.set("view engine","jade");app.use(logger("dev"));app.use(express.json());app.use(express.urlencoded({extended:false}));app.use(cookieParser());app.use(express.static(path.join(__dirname,"public")));app.use("/",indexRouter);//追記ここからapp.use("/loginfail",loginFRouter);app.use("/login",loginRouter);//認証セクションvarpassport=require("passport");const{token}=require("morgan");varOpenidConnectStrategy=require("passport-openidconnect").Strategy;app.use(passport.initialize());app.use(passport.session());passport.use(newOpenidConnectStrategy({issuer:"http://localhost:8080/openam/oauth2",authorizationURL:"http://localhost:8080/openam/oauth2/authorize",tokenURL:"http://localhost:8080/openam/oauth2/access_token",userInfoURL:"http://localhost:8080/openam/oauth2/userinfo",clientID:"sampleRP",clientSecret:"RP_PASSWORD",callbackURL:"http://localhost:3000/oauth2callback",scope:["openid","email","profile"],},function(issuer,sub,profile,jwtClaims,accessToken,refreshToken,tokenResponse,done){//認証成功したらこの関数が実行される//ここでID tokenの検証を行うconsole.log("issuer: ",issuer);console.log("sub: ",sub);console.log("profile: ",profile);console.log("jwtClaims: ",jwtClaims);console.log("accessToken: ",accessToken);console.log("refreshToken: ",refreshToken);console.log("tokenResponse: ",tokenResponse);returndone(null,{profile:profile,accessToken:{token:accessToken,scope:tokenResponse.scope,token_type:tokenResponse.token_type,expires_in:tokenResponse.expires_in,},idToken:{token:tokenResponse.id_token,claims:jwtClaims,},});}));passport.serializeUser(function(user,done){//userにはprofileが入るdone(null,user);});passport.deserializeUser(function(obj,done){done(null,obj);});app.get("/auth/openidconnect",passport.authenticate("openidconnect"));app.get("/oauth2callback",passport.authenticate("openidconnect",{failureRedirect:"/loginfail",}),function(req,res){// Successful authentication, redirect home.console.log("認可コード:"+req.query.code);req.session.user=req.session.passport.user.displayName;res.redirect("/login");});//ここまで// catch 404 and forward to error handlerapp.use(function(req,res,next){next(createError(404));});// error handlerapp.use(function(err,req,res,next){// set locals, only providing error in developmentres.locals.message=err.message;res.locals.error=req.app.get("env")==="development"?err:{};// render the error pageres.status(err.status||500);res.render("error");});module.exports=app;

login成功後は、/loginというページに遷移させる予定なので、
views/login.jade
routes/login.js
をそれぞれ追加します。

views/login.jade
extends layout

block content
  h1= title
  p Welcome to #{title}
  p login成功!
routes/login.js
varexpress=require("express");varrouter=express.Router();/* GET home page. */router.get("/",function(req,res,next){res.render("login",{title:"ログイン"});});module.exports=router;

login失敗時のページも作っておきます。
views/loginfail.jade
routes/loginfail.js

views/loginfail.jade
extends layout

block loginfail
  block content
  h1= title
  p Welcome to #{title}
  p Login失敗
routes/loginfail.js
varexpress=require('express');varrouter=express.Router();/* GET home page. */router.get('/',function(req,res,next){res.render('loginfail',{title:'ログインできなかったよ'});});module.exports=router;

これでRPの作成は完了です。
最終的にはこんな感じのディレクトリ構成になりました。

$lsapp.js                  node_modules            package.json            routes
bin                     package-lock.json       public                  views
$ls views 
error.jade      index.jade      layout.jade     login.jade      loginfail.jade
$ls routes 
index.js        login.js        loginfail.js

RPの登録

OpenAMにRPを登録します。
OpenAMにAdminでログイン後、Top Level Realm(トップレベルレルム)のApplications>OAuth2.0を選択します。
スクリーンショット 2020-08-24 21.03.48.png

エージェントの新規をクリック
スクリーンショット 2020-08-24 21.07.28.png

エージェントの名前とパスワードの入力を求められるので、
今回は下記のように入力し、作成を押します。

名前:sampleRP
パスワード:password

このパスワードは、先ほど作成したapp.js内のRP_PASSWORDにあたります。
作成を押した後、メインページに戻るので、エージェントから、先ほど作成したエージェントを選択し、設定を追加していきます。

項目設定内容
リダイレクトURIhttp://localhost:3000/oauth2callback
スコープopenid, email, profole
Token Endpoint Authentication Methodclient_secret_post

そのほかの設定は、デフォルトのまま。

スクリーンショット 2020-08-24 21.33.22.png

スクリーンショット 2020-08-24 21.34.39.png

設定追加後、保存を押して登録完了です。

動作確認

早速RPを動かしてみます。

$npm start

RPにアクセス!
http://localhost:3000/

SSO連携のリンクを押してみるとOpenAMのログイン画面に遷移します。
スクリーンショット 2020-08-24 20.26.52.png

初期設定で作成したアカウントのID/PWを入れてログイン!
アカウントを作成した記憶がない方はデフォルトの下記アカウントでもログインできるはずです。
ID: demo
password: changeit
ログイン後、個人情報提供の同意画面に遷移するのでAllowを選択します。

スクリーンショット 2020-08-24 20.29.13.png

ログイン成功画面に遷移しました。
スクリーンショット 2020-08-24 21.38.26.png

ターミナルにこんな感じに出力されていれば認証成功です。

スクリーンショット 2020-08-24 20.34.39.png
これでアクセストークン、IDトークンが取得できているはずなので、
この後、ユーザーの情報を取得したい場合は、OpenAMのユーザー情報エンドポイントにアクセストークンを GETで渡せば大丈夫なはずです。
参考:
https://backstage.forgerock.com/docs/am/5/oauth2-guide/#oauth2-byo-client

以上になります。

お疲れ様でした!

参考

[1] 株式会社オージス総研 テミストラクトソリューション部 氏縄 武尊."第三回 Relying Party の実装例 ~passport~".オブジェクトの広場.2016-03-10,(参照2020-08-24)
[2] Open Source Solution Technology Corporation.学認Shibboleth ShibbolethとOpenAMを連携させて学外と学内をシングルサインオン.2011
[3] ForgeRock."exampleOAuth2Clients/node-passport-openidconnect".Github.2020-3-25,(参照2020-08-24)

Asciidoctor.jsでプレビューしながら編集する

$
0
0

AsciiDoc の処理系といえば、Ruby の Asciidoctor1が有名です。しかし、JavaScript な Asciidoctor.js2もあります。本記事は後者を使ってみた記録です。

関連

バージョン

$asciidoctor -vAsciidoctor 2.0.10 [https://asciidoctor.org]
Runtime Environment (ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19]) (lc:UTF-8 fs:UTF-8 in:UTF-8 ex:UTF-8)

$npm -v6.14.8

きっかけ

Ruby 版の Asciidoctor には監視機能がありません。つまり、AsciiDoc ファイルに変更があった際、自動的に変換処理が行われるようにするオプションを、asciidoctorコマンドに指定することはできません。

$asciidoctor --watch main.adoc # こうできたらいいなぁ……

監視機能を進めると、プレビュー機能になります。ここでのプレビュー機能とは、生成結果が変更されたタイミングでその表示に反映されることです。HTML ファイルの場合、ブラウザで再読込する手間が省けます。

今回は HTML ファイルを生成し、プレビューすることを考えます。

Ruby 版 Asciidoctor で

Editing AsciiDoc with Live Preview3に紹介されていますが、Firefox 79.0 (64bit) では拡張機能が使えなくなっているようです。ただ、guardという gem4は AsciiDoc から HTML への変換過程で使えそう。

asciidoctor, guard, guard-shellをインストールします。

$bundle init
$bundle add asciidoctor guard guard-shell

main.adocという AsciiDoc ファイルを監視します。

Guardfile
require'asciidoctor'guard:shelldowatch('main.adoc'){|m|Asciidoctor.convert_filem[0]}end

Guard を起動します。

$guard

AsciiDoc ファイルに変更があるたびに、HTML ファイルが更新されるようになりました。

live-server登場

npm にlive-server5というパッケージがあり、これでプレビューを実現できます。使い方は簡単。

$live-server

guard周りでもできそうですが、これが明快かと思います。

Asciidoctor.js で

live-serverで視点が変わりました。監視はnpm-watch6に任せましょう。

$npm i -D asciidoctor live-server npm-watch

package.jsonに npm scripts7を書きます。

package.json
{"watch":{"convert":"main.adoc"},"scripts":{"start":"live-server","watch":"npm-watch","convert":"asciidoctor main.adoc"},"devDependencies":{"asciidoctor":"^2.2.0","live-server":"^1.2.1","npm-watch":"^0.7.0"}}

注意:$ npm run convertで起動される Asciidoctor は Asciidoctor.js の方です。実際、$ npx asciidoctor --helpで Asciidoctor.js のヘルプが表示されます。

AsciiDoc ファイルも準備しましょう。

main.adoc
= Hello

* ワン
* ツー
* スリー

用意するファイルはこれだけ:

$ls-1main.adoc
package.json

監視、プレビュー……

$npm run watch # terminal 1$npm run start # terminal 2$vim main.adoc # terminal 3

before

そして編集……

main.adoc
= Hello

_Happy AsciiDocing!_

after

DynamoDB Localトラブルシューティング(Node.js + TypeScript)

$
0
0

Node.js + TypeScript(Dockerコンテナ)からDynamoDB Localへ接続、操作をする際に発生したトラブルの備忘録です。

DynamoDB Localとは?

AWS上のDynamoDBにアクセスすることなく、DynamoDBを利用するアプリケーションの開発・テストをすることが可能になります。

DynamoDB Localの設定(ダウンロード版)

背景

前提としてNode.js + TypeScriptのプログラムはECS on Fargateのタスクスケジューラで定期実行するプログラムです。

今回DynamoDB Localを導入するのは、開発時にAWSのDynamoDBを利用せず、ローカル環境で全て完結させたいというのが理由です。

忙しい人のために

トラブル対応後のファイル構成、操作手順が下記になります。

まとめ📖 (ファイル一覧/操作手順)

事前準備

本実装に入る前にDynamoDB LocalをDockerで立ち上げて、AWS公式のNode.jsとDynamoDBのチュートリアルを参考に実装を進めることに。

ファイル構成

$ tree
.├── Dockerfile
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

DynamoDB Local立ち上げ

最初はdocker-composeを利用せず、下記コマンドでDynamoDB Localを立ち上げていました。(なるべく本番環境に不要なファイルを作りたくなかった)

$ docker run --name dynamodb -p 8000:8000 amazon/dynamodb-local

トラブル(発生順)

その1 : ~ is not assignable to parameter of type 'ConfigurationOptions & ConfigurationServicePlaceholders & APIVersions'

import*asAWSfrom'aws-sdk';AWS.config.update({region:'us-west-2',endpoint:'http://localhost:8000'});constdynamodb=newAWS.DynamoDB.DocumentClient();
TSError: ⨯ Unable to compile TypeScript:
src/index.ts(14,3): error TS2345: Argument of type'{ region: string; endpoint: string; accessKeyId: string; secretAccessKey: string; }' is not assignable to parameter of type'ConfigurationOptions & ConfigurationServicePlaceholders & APIVersions'.
  Object literal may only specify known properties, and 'endpoint' does not exist in type'ConfigurationOptions & ConfigurationServicePlaceholders & APIVersions'.

解決法

TypeScriptで実装しているため、型定義によるエラーが発生。詳細は省きますが、下記のようにプログラムを書き換えることで解消されました。

修正ポイント : ServiceConfigurationOptionsを指定する

import*asAWSfrom'aws-sdk';import{ServiceConfigurationOptions}from'aws-sdk/lib/service';constserviceConfigOptions:ServiceConfigurationOptions={region:'us-west-2',endpoint:'http://localhost:8000'};AWS.config.update(serviceConfigOptions);constdynamodb=newAWS.DynamoDB.DocumentClient();

その2 : Missing credentials in config

import*asAWSfrom'aws-sdk';import{ServiceConfigurationOptions}from'aws-sdk/lib/service';constserviceConfigOptions:ServiceConfigurationOptions={region:'us-west-2',endpoint:'http://localhost:8000'};AWS.config.update(serviceConfigOptions);constdynamodb=newAWS.DynamoDB.DocumentClient();
Error Error: connect ECONNREFUSED 169.254.169.254:80
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16){
  message: 'Missing credentials in config, if using AWS_CONFIG_FILE, set AWS_SDK_LOAD_CONFIG=1',
  errno: 'ECONNREFUSED',
  code: 'CredentialsError',
  syscall: 'connect',
  address: '169.254.169.254',
  port: 80,
  time: 2020-08-23T23:30:29.548Z,
  originalError: {
    message: 'Could not load credentials from any providers',
    errno: 'ECONNREFUSED',
    code: 'CredentialsError',
    syscall: 'connect',
    address: '169.254.169.254',
    port: 80,
    time: 2020-08-23T23:30:29.547Z,
    originalError: {
      message: 'EC2 Metadata roleName request returned error',
      errno: 'ECONNREFUSED',
      code: 'ECONNREFUSED',
      syscall: 'connect',
      address: '169.254.169.254',
      port: 80,
      time: 2020-08-23T23:30:29.547Z,
      originalError: [Object]
    }}}

AWS公式のNode.jsとDynamoDBのチュートリアルをTypeScriptに置き換えただけですが、動作せず・・・。

解決法

設定値にAccess KeySecret Access Keyがないとエラーになるようです。

修正ポイント : キーにaccessKeyIdsecretAccessKeyを追加、値は任意の値でOK

import*asAWSfrom'aws-sdk';import{ServiceConfigurationOptions}from'aws-sdk/lib/service';constserviceConfigOptions:ServiceConfigurationOptions={region:'us-west-2',endpoint:'http://localhost:8000',accessKeyId:'fakeAccessKeyId',secretAccessKey:'fakeSecretAccessKey'};AWS.config.update(serviceConfigOptions);constdynamodb=newAWS.DynamoDB.DocumentClient();

その3 : connect ECONNREFUSED 127.0.0.1:8000

import*asAWSfrom'aws-sdk';import{ServiceConfigurationOptions}from'aws-sdk/lib/service';constserviceConfigOptions:ServiceConfigurationOptions={region:'us-west-2',endpoint:'http://localhost:8000',accessKeyId:'fakeAccessKeyId',secretAccessKey:'fakeSecretAccessKey'};AWS.config.update(serviceConfigOptions);constdynamodb=newAWS.DynamoDB.DocumentClient();
Error Error: connect ECONNREFUSED 127.0.0.1:8000
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16){
  errno: 'ECONNREFUSED',
  code: 'NetworkingError',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 8000,
  region: 'us-west-2',
  hostname: 'localhost',
  retryable: true,
  time: 2020-08-24T23:03:41.246Z
}

localhostではDockerホストにアクセスできずエラーとなります(そりゃそうだ)

解決法

解決方法としてはコンテナ間の通信を可能とする、もしくはDockerホスト(Mac)を経由する2通りが考えられますが、今回は後者で解決できました。

Docker for Mac上のコンテナから、Mac上のアプリケーションに簡単に接続する方法

修正ポイント : endpointの値を http://docker.for.mac.localhost:8000に変更

import*asAWSfrom'aws-sdk';import{ServiceConfigurationOptions}from'aws-sdk/lib/service';constserviceConfigOptions:ServiceConfigurationOptions={region:'us-west-2',endpoint:'http://docker.for.mac.localhost:8000',accessKeyId:'fakeAccessKeyId',secretAccessKey:'fakeSecretAccessKey'};AWS.config.update(serviceConfigOptions);constdynamodb=newAWS.DynamoDB.DocumentClient();

その4 : ResourceNotFoundException: Cannot do operations on a non-existent table

DynamoDB Localへの接続で上手くいったので、テストデータを追加してプログラムを作成していきます。

追加は下記にアクセスして操作します。

http://localhost:8000/shell/

操作方法:DynamoDBローカルをDockerコンテナとして動かす

ブラウザ上で追加したデータをNode.js + TypeScript(Dockerコンテナ)から取得してみましたが掲題のエラーに・・・。

テーブル一覧を取得しても、ブラウザ上で追加したテーブルがない状態・・・。

🤔 🤔 🤔 🤔 🤔

解決法

DynamoDB Localのデータファイルが設定されているアクセスキー(Access Key)リージョン(Region)によって決まってくるようです。

[Access Key]_[Region].db

DynamoDB LocalでgetItemしたら、ResourceNotFoundException: Cannot do operations on a non-existent table が返ってきた

要するにアクセスキーリージョンの値をDynamoDB Localの設定値とプログラムで指定する値を揃える必要があります。

DynamoDB Localのアクセスキーはブラウザ上のオプションから変更が可能です。(おそらくDynamoDB Localを立ち上げる際に環境変数で指定することも可能?)

dynamodb-local.jpg

ちゃんと書いてありますね。 the DB file is based upon the Access Key ID

プログラム上でアクセスキーをfakeAccessKeyIdと指定しているので、同じ値をブラウザ上で設定すると今度は上手くいきました。

ただ、今のままではDynamoDB Localを立ち上げるたびにAccess Keyを指定する必要があるので、データの永続化も含めdocker-composeで対応します。

ルートディレクトリにdynamodblocalというDynamoDB Local用のディレクトリを新規に作成し、その中にdocker-compose.ymlとdataディレクトリを追加します。

$ tree
.├── Dockerfile
├── dynamodblocal
│   ├── data/
│   └── docker-compose.yml
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

./dynamodblocal/data/ディレクトリはDynamoDB Localを起動する前に作成しておく

dynamodblocal/docker-compose.yml
version: "3"

services:
  dynamodb:
    image: amazon/dynamodb-local
    volumes:
      - ./data:/home/dynamodblocal/data
    ports:
      - "8000:8000"
    command: -jar DynamoDBLocal.jar -dbPath ./data -sharedDb

DynamoDB Localを起動時にsharedDBオプションを指定することで、アクセスキーやリージョンに関係なくアクセスすることが可能になります。

まとめ

$ tree
.├── Dockerfile
├── dynamodblocal
│   ├── data
│   │   └── shared-local-instance.db
│   └── docker-compose.yml
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

./data/shared-local-instance.dbはコンテナ起動後、自動的に生成される(dataディレクトリだけ用意しておけばおk)

dynamodblocal/docker-compose.yml
version: "3"

services:
  dynamodb:
    image: amazon/dynamodb-local
    volumes:
      - ./data:/home/dynamodblocal/data
    ports:
      - "8000:8000"
    command: -jar DynamoDBLocal.jar -dbPath ./data -sharedDb
src/index.ts
import*asAWSfrom'aws-sdk';import{ServiceConfigurationOptions}from'aws-sdk/lib/service';letserviceConfigOptions:ServiceConfigurationOptions={region:'us-west-2',endpoint:'http://docker.for.mac.localhost:8000',accessKeyId:'fakeMyKeyId',secretAccessKey:'fakeSecretAccessKey'};AWS.config.update(serviceConfigOptions);constdynamodb=newAWS.DynamoDB.DocumentClient();

起動

$ cd dynamodblocal/
$ docker-compose up -d

停止

$ docker-compose down

Node.jsでExpress.jsを使ってpng画像を動的に表示するサンプル

$
0
0

画像ファイルを動的に表示するコードです。なかなか見つからなかったのでメモしておきます。

express.js
constexpress=require('express');constfs=require('fs');constapp=express();app.get('/image',(req,res)=>{console.log('image');fs.readFile('./example.png',(err,data)=>{res.type('png');res.send(data);});});app.listen('3000',()=>{console.log('Application started');});

スクリプトを実行し

% node express.js
Application started
image

ブラウザで下記URLにアクセスすると画像が表示されます
http://localhost:3000/image

スクリーンショット 2020-08-25 12.40.21.png

ちなみにディレクトリのファイル一覧はこんな感じです

% ls
example.png     package-lock.json
express.js      package.json
node_modules

mongoDBのドキュメント削除方法

$
0
0

初めまして!初投稿です。
こうきといいます。

mongoDBでドキュメント内容を削除する方法が日本語で簡潔に纏められている記事がなかったので、
備忘録にと投稿することにしました。

環境

ターミナル : iTerm
mongoDB : ver. 4.4.0

DB起動 ログイン

mongo  // mongoDB起動

use admin  // adminの部分は各々のDB名を記入

コレクション検索〜ドキュメント削除まで

・コレクション検索
show collections  // コレクション検索
・コレクションの中身(ドキュメント)表示
db.inventory.find() //inventoryの部分は各々のコレクション名を記入
・ドキュメント削除

inventoryの部分はコレクション名を記入
mongoDBのバージョンが3未満なら波括弧{}は不要です。

db.inventory.deleteMany({}) //すべてのドキュメントを削除
db.inventory.deleteOne({}) //指定したドキュメントを削除

例:db.inventory.deleteOne({ status: "D" }) //statusが"D"のドキュメントを削除

参考:https://docs.mongodb.com/manual/tutorial/remove-documents/

【AWS・Lambda】Lambdaから別リージョンのサービスを使用するための設定

$
0
0

はじめに

Labmda関数から別のAWSサービスを使用したい...
でも、現在リージョンと別リージョンのサービスを使いたい!

そんな時に使用する設定をご紹介します。

Pinpointなど、まだ東京リージョンで使用できないサービスを使う時にも役立ちます。

方法

以下はNodeの例です。

constAWS=require('aws-sdk');// 以下を追加AWS.config.update({region:'us-west-2'});

まとめ

AWS.config.updateすることで、リージョンを切り替えできます。


Serverless Frameworkを使用して、AWS上のLambdaにデプロイするまで【開発環境構築含む】

$
0
0

概要

タイトルの通り、Serverless Frameworkを使用して、AWS上のLambdaにデプロイするまでの開発環境構築手順
最終的には、NestJSのアプリケーションをLambdaにデプロイして「Hello World」を表示させたいと思います。

1. Serverless Frameworkをインストールする前に

事前に開発環境を整えます。本記事の開発端末はMacです。
基本的なインストールが完了している方は、Serverless Frameworkをインストールするところから読んでいただければと思います。

Homebrewのインストール

ホームページにあるスクリプトを実行する

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

nodebrewのインストール

「Homebrew」が使えるようになったので、次に「nodebrew」をインストールします。
nodebrewはNode.jsのインストールから複数のバージョンを管理・切り替えできるツールになります。

$ brew install nodebrew

Node.jsのインストール

必要なバージョンを指定してインストールします。

$ nodebrew install-binary latest

インストール直後はcurrent: noneとなっているため、必要なバージョンを有効化します。

nodebrew use v x.x.x

参考記事:https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09

AWSCLIのインストール

Macの場合、pipが初めからインストールされているため、pipを使用してAWSCLIをインストールしたいと思います。

sudo pip install awscli

Gitのインストール

GitはHomebrewを使用してインストールします。

参考:https://qiita.com/micheleno13/items/133aee005ae37c28960e

Gitをインストールしてもxcode-selectがインストールされていないと使用できない場合があります。

xcode-select --install

参考:https://qiita.com/royroy/items/338362362de73a94fc0c

必要アカウントの用意(登録)

  1. awsアカウント

    1. アカウントの作成
      https://aws.amazon.com/jp/register-flow/
    2. IAMユーザの作成
      https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html
    3. IAM アクセスキーの発行
      作成したIAMユーザを選択し、「認証情報」から「アクセスキーの作成」を選択し、アクセスキーとシークレットキーを取得します。(後ほど使用するのでどこかに保管してください)
  2. GitHub(コード管理する場合のみ)

GitHub(アカウント)登録:https://github.co.jp/
Gitについて参考(全般):https://employment.en-japan.com/engineerhub/entry/2017/01/31/110000

2. Serverless Frameworkのインストール

参考:https://dev.classmethod.jp/cloud/aws/easy-deploy-of-lambda-with-serverless-framework/

npmからServerless Frameworkをインストールします。

$ npm install -g serverless

完了したら動作確認します。(バージョン確認)

$ serverless -v
Framework Core: 1.59.3
Plugin: 3.2.5
SDK: 2.2.1
Components Core: 1.1.2
Components CLI: 1.4.0

AWSアカウントの設定

Serverless FrameworkからLambdaなどAWSのリソースにデプロイする際にアクセス権限が必要なための設定です。
先ほどAWSアカウントを登録した際のIAMアクセスキーの情報をServerless Frameworksに設定します。

sls config credentials --provider aws --key XXXXXXXXXXXXEXAMPLE --secret XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXEXAMPLEKEY

3. NestJS を Lambda にデプロイする

基本的な開発環境が整ったので、実際にServerless Frameworkを使用してデプロイを行いたいと思います。
以下リポジトリをそのまま引用します。
https://github.com/rdlabo/serverless-nestjs

NestJSのインストール

今回はNestJSのアプリケーションを使用するためインストールします。

$ npm install @nestjs/cli serverless -g

ソースをローカルにコピー

Git Cloneでローカルにソースをコピーします。

git clone https://github.com/rdlabo/serverless-nestjs.git

デプロイ時に必要なパッケージをインストール

$ npm install

ローカルで「Hello World」が表示されるか確認

$ npm start

ブラウザで確認します。
http://localhost:3000/
スクリーンショット 2019-12-13 4.03.51.png

Lambdaデプロイ

$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
~~~~~~~~~~~~~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~~~~
endpoints:
  ANY - https://jpvi3b5i8a.execute-api.us-east-1.amazonaws.com/dev/
  ANY - https://jpvi3b5i8a.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.

生成されたエンドポイントで確認します。
※デプロイしたサーバによってエンドポイントは異なります。
https://jpvi3b5i8a.execute-api.us-east-1.amazonaws.com/dev/
スクリーンショット 2019-12-13 3.59.58.png
AWSコンソール上でリソースが作成されたことを確認します。
リージョンはバージニア北部(us-east-1)に作成されています。
スクリーンショット 2019-12-13 3.56.46.png
確認したらリソースは削除してください。

Node.jsでGoogle Drive上のファイルをダウンロードする (Google Drive API v3)

$
0
0

1年くらい前にGoogle Drive関連の記事を書いてたけど、久々に触りたくなったので調査再開。

メソッドはFiles: getになります。

ライブラリの書き方だとdrive.files.get()です。

スコープ指定

スコープの指定は割と多め。

https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/drive.metadata.readonly
https://www.googleapis.com/auth/drive.appdata
https://www.googleapis.com/auth/drive.metadata
https://www.googleapis.com/auth/drive.photos.readonly

ダウンロードする

チュートリアルfunction listFilesの箇所を書き換えて利用してみます。

機能的にはfunction dlFileとかにリネームした方が良いでしょうが一旦動かす体なのでスルー

fileIdにGoogle DriveのファイルのIDを指定しましょう。

またtest.mp4としてますが、ファイルの種類によって拡張子は変えてください。

dj.js
asyncfunctionlistFiles(auth){constdrive=google.drive({version:'v3',auth});constfileId=`xxxxxxxxxxxx`;//GoogleDriveのファイルIDを指定constdest=fs.createWriteStream('test.mp4','utf8');try{constres=awaitdrive.files.get({fileId:fileId,alt:'media'},{responseType:'stream'});res.data.on('data',chunk=>dest.write(chunk));res.data.on('end',()=>dest.end());//保存完了}catch(err){console.log('The API returned an error: '+err);}}

けっこうシンプルに書けますね。
実行すると手元にtest.mp4が保存されます。

おまけ: プログレス表示

公式サンプルを参考にして書くとこんな感じになります。

dl.js
//省略asyncfunctionlistFiles(auth){constdrive=google.drive({version:'v3',auth});constfileId=`xxxxxxxxxxxx`;//GoogleDriveのファイルIDを指定constdest=fs.createWriteStream('test.mp4','utf8');letprogress=0;try{constres=awaitdrive.files.get({fileId:fileId,alt:'media'},{responseType:'stream'});res.data.on('end',()=>console.log('Done downloading file.')).on('error',err=>console.error('Error downloading file.')).on('data',d=>{progress+=d.length;if(process.stdout.isTTY){process.stdout.clearLine();process.stdout.cursorTo(0);process.stdout.write(`Downloaded ${progress} bytes`);}}).pipe(dest);}catch(err){console.log('The API returned an error: '+err);}}
  • 実行

こんな感じでプログレス表示があって良いですね。

$ node dj.js
Downloaded 79767428 bytesDone downloading file.

所感

実際に使うときには、

  • 1. drive.files.list()でフォルダ内のファイル一覧及びIDなど取得
  • 2. drive.files.get()でダウンロード

みたいな流れが多いかなぁと思います。

ファイル保存でStreamを使うあたりがNode.jsっぽさを感じますね。

参考: Node.js Stream を使いこなす

Node.jsでGoogle Driveにファイルをアップロードする (Google Drive API v3)

$
0
0

Google Driveへのファイルアップロードも試してみます。

メソッドはFiles: createになります。

ライブラリの書き方だとdrive.files.create()です。

.uploadではないので注意。

スコープ指定

https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.appdata

フォルダを指定してアップロード

チュートリアルfunction listFilesの箇所を書き換えて利用してみます。

機能的にはfunction createFileとかにリネームした方が良いでしょうが一旦動かす体なのでスルー

folderIdにGoogle Driveのアップロード先のフォルダのIDを指定しましょう。

パラメータでresource.parentsを指定していますが、ここが配列なので複数のフォルダに配置もできそうですね。

参考: Create a file in a folder

またtest.mp4としてますが、ファイルの種類によって拡張子は変えてください。

create.js
//省略asyncfunctionlistFiles(auth){constdrive=google.drive({version:'v3',auth});constfileName=`test.mp4`;constfolderId='xxxxx';constparams={resource:{name:fileName,parents:[folderId]},media:{mimeType:'video/mp4',body:fs.createReadStream(fileName)},fields:'id'};constres=awaitdrive.files.create(params);console.log(res.data);};

実行すると手元のtest.mp4が指定したGoogle Driveのフォルダにアップロードされます。

スクリーンショット 2020-08-26 2.22.53.png

おまけ: プログレス表示

公式サンプルを参考にして書くとこんな感じになります。

dl.js
//省略constfileSize=fs.statSync(fileName).size;constres=awaitdrive.files.create(params,{onUploadProgress:evt=>{constprogress=(evt.bytesRead/fileSize)*100;readline.clearLine();readline.cursorTo(0);process.stdout.write(`${Math.round(progress)}% complete`);},});console.log(res.data);
  • 実行

こんな感じのプログレス表示になります。

$ node create.js
・
・
・
mplete99% complete99% complete99% complete99% complete99% complete99% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete100% complete{id: 'xxxxxxxxxxxxxx'}

よもやま

res.dataのあたりでなんとく分かる人はいるかもしれないですが、こちらにコメントがあるようにaxiosを使ってアップロードしてるみたいですね。

そして公式ドキュメントがけっこうアップデートされていて公式のコード使えば良さそうってのに後から気づいた

Cloud Functions for FirebaseでNode.js 12を利用する方法

$
0
0

公式ドキュメントの「ランタイム オプションを設定する」の項目には Node.js のバージョン 12 が使えることが書かれていますが、その利用方法までは書かれていなかったのでこの記事で説明します。


一見、ドキュメントにあるように

  "engines": {"node": "10"}

の箇所を12とすれば動きそうですが、ここに書いても動作しません。

Node.js の 12 を利用するためには、firebase-toolsのバージョンを8.6.0以上に上げたうえでfirebase.json"runtime": "nodejs12"を指定しましょう。

{
  "functions": {
    "runtime": "nodejs12"
  }
}

参考:firebase/firebase-tools/releases/tag/v8.6.0


Firebase 関連のパッケージはここ最近バージョンアップが頻繁に行われている印象で、ドキュメントも追いついていないっぽいので現在の情報が知りたい方はパッケージのリリース情報にも目を通していったほうがいいかもしれません。

世界初!「ラブライブ!」と「ワイルド・スピード」の聖地を教えてくれるLINEbot!

$
0
0

「ラブライブ!」大好き!「ワイルド・スピード」も大好き!
そんな欲張りさん向けにLINEbotを作りました。よかったら最後までご覧ください♪

作品の背景

聖地巡礼が好きな私。ロサンゼルスにワイスピの聖地巡礼に行きたいと考えているものの、コロナで旅行も行けず・・・。

「ワイルド・スピード」シリーズは、スピンオフも含めて、これまでに9作品上映されているのですが、
3作目となる「ワイルド・スピードX3 TOKYO DRIFT」は、なんと東京が舞台!

東京なら巡礼できる!と思い、ワイスピ聖地巡礼bot東京版を作ることにしました!!

ところがどっこい!!
意気揚々と調べたものの、CGで作られていたり、すでに取り壊されていたりと、聖地情報が希薄・・・。泣

これではbotとして成り立たないので、大好きな「ラブライブ!」と組み合わせ、世界初のコラボレーションを実現しました。

デモ動画

完成したものがコチラです!
位置情報を送ると、最寄りの聖地を教えてくれます。

ワイスピの聖地は本当少ないので、隠しコマンドみたいになってます。笑

作る時のポイント

LINEbotの作り方

いつもこちらの記事を参考にさせていただいております!
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest

2点間の距離の測り方

こちらが1番苦戦しました。。。
何から手をつけていいのかわからずで、Qiitaにも質問記事を投稿しました。

▶投稿した質問:LINEbotで現在地情報を送ったら、近くのスポットを返すコードを書きたい【Javascript/Node.js】

すると、@uasiさんがとても親切に教えていただき、すぐに解決することができました。
本当にありがとうございます。

Qiitaの回し者ではないですが、質問投稿するの本当勉強になります。
初心者エンジニアの方にオススメです。

できたコードがこちら。

// 角度をラジアンに変換functiontoRad(deg){returndeg/180*Math.PI;}// 2点間の大円距離を計算(返り値は km 単位)// 参考: http://www.orsj.or.jp/archive2/or60-12/or60_12_701.pdffunctiondistance(lat1,lon1,lat2,lon2){lat1=toRad(lat1);lon1=toRad(lon1);lat2=toRad(lat2);lon2=toRad(lon2);return6370*Math.acos(Math.sin(lat1)*Math.sin(lat2)+Math.cos(lat1)*Math.cos(lat2)*Math.cos(lon1-lon2));}// 与えた緯度経度の地点からもっとも近い場所を返すfunctiongetNearestLocation(lat,lon){// dataWithDistance は [ [場所1, 指定地点と場所1の距離], [場所2, 指定地点と場所2の距離], ... ]letdataWithDistance=data.map(item=>[item,distance(lat,lon,item.lat,item.lon)]);// 距離がもっとも近い場所を取り出す// reduce() の解説: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reducereturndataWithDistance.reduce((nearest,current)=>current[1]<nearest[1]?current:nearest)[0]||null;}

これで最寄りのスポットを抽出することができるようになりました。

▶参考記事:【LINE BOT】位置情報と目的地の距離を発信

メッセージの見やすさ

抽出した最寄りスポットを見やすくするために、
説明文・画像・地図をメッセージとして返せるようにしました。

Image from Gyazo

違うメッセージタイプのものを一緒に送る方法が最初わからなかったのですが、
シンプルに[]でまとめれば上手く行きました♪

asyncfunctionhandleEvent(event){// 位置情報のみに入力制限if(event.type!=='message'||event.message.type!=='location'){returnPromise.resolve(null);}// 取得した位置情報をログに表示console.log(event.message.latitude+' : '+event.message.longitude);constnearestLocation=getNearestLocation(event.message.latitude,event.message.longitude);console.log(nearestLocation);returnclient.replyMessage(event.replyToken,[{type:'text',text:'【作品】'+nearestLocation.category+''+nearestLocation.memo,},{type:'image',originalContentUrl:nearestLocation.image,previewImageUrl:nearestLocation.image,},{type:'location',title:nearestLocation.spot,address:nearestLocation.address,latitude:nearestLocation.lat,longitude:nearestLocation.lon,}]);}

改善点

▶最寄りだけじゃなくて、半径●キロ以内とかにある全スポットを出せるといいな!
▶位置情報だけじゃなくて、キーワード検索もできるようにしたい
▶もっと自力で検索して実装できるようにしたい!

今回も皆さまのおかげで最低限の形にはなりましたが、まだまだスキルが足りないので精進したいです・・・!

おわりに

まだ未完成な部分があるので、完成したらデプロイして、ソースコードとQRコードも載せる予定・・・!
聖地ではなく、好きなお店とかにアレンジして作るとより実用性高いものになると思うので、
ぜひ皆さんも作ってみてください~♪

(*^^)v「よろしければLGTMも宜しくお願いします!」

なぜAPサーバーをWebサーバーとして利用しないのか

$
0
0

はじめに

よく聞く話として、「Web3層構造に分けよう」というものがある。Webサーバー、APサーバー、データベースだったと思う。そこで、こんな疑問が生じた。「APサーバーでもHTTPリクエストの処理ができるなら、なんでWebサーバーが必要になるんだ?」と。調べた。

目次

  1. Webサーバーとは?
  2. APサーバーとは?
  3. WebサーバーとAPサーバーを分けるもう1つの理由

Webサーバーとは?

まずWebサーバーとして、ApacheとNginxの2つが挙げられることが多い。この2つについて理解する。次の記事を読んでみて欲しい。

 1. ApacheとNginxについて比較
 2. Nginxのアーキテクチャを理解する

1つ目の記事では、Apacheがマルチプロセスのプロセス駆動アーキテクチャであること、Nginxがシングルスレッドモデルのイベント駆動アーキテクチャであることがわかる。

2つ目の記事では、その仕組みについて図を通して利用されている。

ここで、スレッドとプロセスってなんだったっけ?となる場合、次の記事を参考にしてみて欲しい。

 3. 【図解】CPUのコアとスレッドとプロセスの違い・関係性、同時マルチスレッディング、コンテキストスイッチについて

ここで大事なのは、

CPUコアとは実際に命令を行う部品のことで、SMT登場前においては

『CPUコア数=同時に実行できる命令の数』

でした。

実行中のプログラムは『プロセス』と呼ばれ、プロセスは 1 つ以上の『スレッド』を持ちます。このスレッドが CPU コアに命令を与えますので、 CPU コア数 = 同時実行できるスレッド数 でした。

Apacheは、マルチプロセスのプロセス駆動アーキテクチャーであるから、HTTPリクエストとプロセスが1対1で対応する。結果、プロセス数の上限以上のリクエストを同時に処理することができなくなる。

Nginxは、そもそもがシングルスレッドモデルのイベント駆動アーキテクチャーであるから、プロセスが増えない。その結果、C10K問題を回避することができるというもの。

ノンブロッキングIOに関する個人的に分かりやすかった資料。

4.Apacheコミッターが見た、 Apache vs nginx
5.Nginxが早い理由について調べた(基礎)

ここまでで重要なこととしては、プロセスやスレッドの管理がHTTPリクエストを捌く上で肝心である。ということです。

本題のAPサーバーについて、入っていきます。

APサーバーとは?

Apache TomcatやNode、Puma、UnicornなどのHTTPリクエストから動的コンテンツを生成し、レスポンスとして返す機能を持ったサーバーです。

Web3層構造の各層の説明の参考
 6. ミドルウェア(Web、AP、DB)について知ろう

先ほどの記事で、APサーバーがどんなことをするのかは知ってもらえたかと思います。ここで気になるのが、「Apache TomcatやNodeなどでもHTTPリクエストの処理はできたのに、なぜWebサーバーが必要になるんだ?」というところです。答えは次の記事に載っています。Nodeのついての例となっていますが、後からApache Tomcatの方にも触れていこうと思ってます。

APサーバーをWebサーバーのように利用しない理由
 7. いまさら聞けないNode.js

読んでもらうとわかることとしては、「C10K問題が解決できるのであれば使える可能性がある。しかし、そのためには多くの課題がある。」ということです。

個人的に、重要だと思っているのは、

リソースを回収してくれない

マルチプロセス方式では、リクエストの処理が終わった時点で(=プロセスが終了した時点で)使っていたリソース(メモリーやファイルなど)が自動的に解放されるので、リソースの解放についてプログラマーがあまり意識する必要はありません。

一方、Node.jsでは自動的に解放されないので、明示的に解放しておかないと使われないリソースがどんどん溜まってリソースが枯渇し、新しいリクエストを処理できなくなります。

これもシングルプロセスがゆえの問題です。

というところです。いわゆる、メモリーリークというやつですね。

APサーバーとプロセス数

先ほどまでのところで、NodeがWebサーバーとして適していないことは少し理解してもらえたかもしれません。しかし、Tomcatについては触れていなかったので、ここで触れます。

APサーバーをWebサーバーとして使わないもう1つの理由としては、コネクションの上限があると思います。Tomcatの例を載せます。

 8. 【真夏の夜のミステリー】Tomcatを殺したのは誰だ? (1/3)

ここからわかることとしては、APサーバーにはデフォルトで接続の上限が決められているということです。Nodeも同様です。

 9. NodeJSでの同時接続数について
10. server.maxConnections

Webサーバーに対しては、不特定多数から多くの接続をされることがあります。その際にAPサーバーでリクエストを捌くと不十分な場合が多いということなんですね。

WebサーバーとAPサーバーを分けるもう1つの理由

セキュリティも理由としてあるそうです。

11. WebサーバーとAPサーバの分離について
12. 公開Webサーバ

おわりに

Webサーバーを用意しなければ理由はわかっていませんでした。調べてみた結果、さらに深めるきっかけやキーワードが増えたので、もっと色々なことを知ることができると面白そうですね。

おまけ

おもしろそうな記事:
1. 2015年Webサーバアーキテクチャ序論
2. NGINXのパフォーマンスをスレッドプールで9倍にする
3. Node.jsでのJavaScriptメモリリークを発見するための簡単ガイド

Nginxの補足:Nginxの仕組みについて入門

SMTについて:【図解】ハイパースレッディング(SMT)の仕組み~メリットとデメリット、悪影響や脆弱性などの問題について~

メモリーリークの補足:メモリリーク (memory leak)

東京アラートBOTを作ってみた

$
0
0

東京アラートBOT

LINE BOTを利用し、新型コロナウィルスの新規陽性者数、死亡者数、検査数、東京アラートが出ているかどうかを返してくれるものを作成しました。

東京アラートを発令する基準

作る過程で東京アラートが発令される基準を調べました。

  • 新規陽性者数
  • 新規陽性者における接触不明率
  • 週単位の陽性者増加比
  • 重症患者数
  • 入院患者数
  • PCR検査の陽性者数
  • 受診相談窓口における相談件数

上記の7項目の状況によって検討されるようです。
重傷者が増えていないので、当面は発令されそうにありませんね。

完成品

構成

環境

  • node.js v14.5.0
  • @line/bot-sdk 7.0.0
  • axios 0.19.2
  • express 4.17.1

LINE Messaging API

Node.jsでLINE BOTを作る方法はこちらを参考にしました。

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

データ元

東京における新型コロナウイルスのデータは以下のサイト同じデータです。

東京都新型コロナウイルス感染症対策サイト

ソースコードはMITライセンスで公開されており、誰でも自由に利用することができるそうです。 詳しくは、 GitHub リポジトリをご確認ください。

コード

index.js
"use strict";constexpress=require("express");constline=require("@line/bot-sdk");constaxios=require('axios');constPORT=process.env.PORT||3030;constconfig={channelSecret:'××××××××××××××××',channelAccessToken:'×××××××××××××'};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);asyncfunctionhandleEvent(event){if(event.type!=='message'||event.message.type!=='text'){returnPromise.resolve(null);}if(event.message.text==='こんにちは'){letreplyText='こんにちは。東京の新型コロナ情報LINEボットです。\n\n'+'取得ボタン:\n'+'新規陽性者、死亡者数、検査数を表示します\n'+'※最新のお知らせは19:30分ごろ更新されます。\n\n'+'都庁アイコン:\n'+'東京アラートが発令されているか教えてくれます\n';returnclient.replyMessage(event.replyToken,{type:'text',text:replyText});}lettokyo_alert='';letmes='';if(event.message.text==='東京アラート'){constresponse=awaitaxios.get('https://raw.githubusercontent.com/tokyo-metropolitan-gov/covid19/master/data/tokyo_alert.json');console.log(response.data.alert);constalert=response.data.alert;if(alert===false){tokyo_alert="東京アラートは発令されていません。";console.log(tokyo_alert);}elseif(alert===true){tokyo_alert="東京アラートが発令されました!";console.log(tokyo_alert);}mes='現在は、'+tokyo_alert;returnclient.replyMessage(event.replyToken,{type:'text',text:mes,});}elseif(event.message.text==='最新情報'){constres=awaitaxios.get('https://raw.githubusercontent.com/tokyo-metropolitan-gov/covid19/master/data/news.json');console.log(res.data.newsItems[1].text);consttokyo_data=res.data.newsItems[1].text;mes2=tokyo_data;awaitclient.replyMessage(event.replyToken,{type:'text',text:mes2,});}}app.listen(PORT);console.log(`Server running at ${PORT}`);

感想

今回はリッチメニューを使いました、UIが簡単に作れるのでめちゃめちゃ便利です。サービスを高速で構築できそう。
東京都新型コロナウイルス感染症対策サイトのおかげで、素早く必要なデータを取得できました。

今後もLINE Messaging APIを活用しアウトプットしたいと思います。


初めてのシステム案件で詰まったお話(Heroku + puppeteer)

$
0
0

はじめに

こんにちは!ばーんです

今回は自分が初めて納品したシステム開発のお仕事で得たものを整理していきます。

今回書いていく内容は

  • システム開発全般
  • puppeteer(スクレイピング)に関すること
  • Heroku(PaaS)に関すること

について書いていきます。これからシステム開発していきたい!
と考えている方は、見ていただけると気づきがあるのかなと思います。

結論

ざっくり大切だなと感じたことが3点。

①デバッグ / コードレビューはお金を支払ってでも現役エンジニアにお願いする
(デバッグは詰まりに詰まったか、単純に自分がしているところを見てもらう)
見ている観点がそもそも違うのでとても勉強になります。

②基礎を学ぶことが遠回りのように見えて近道
結局上辺だけの解決方法では解決しないことが殆どでした。
今回で言えば公式ドキュメントなどの一次情報を見る。linuxについて理解を深めるなど。

③簡単なことはGASで解決できる
システム開発これから携わりたい!という方はまずGAS触ればいいと思います。
クライアントの要望の多くは「スプレッドシート + GAS」で解決できました。
GASはできないことが明確なので、見積もりをたてる一つの基準になるかなと思いますし。

また、会社員の方であれば社内のルーチンなどは大抵GAS使えば効率化できます。
信頼を得るまでは時間外で学習としてやってみて完成させる。使ってもらって感触よければ信頼がついて業務時間中にできるようになります。

取り掛かる時の自分の状況 / 案件の概要 / 技術選定 / 結果

取り掛かる時の自分の状況

  • エンジニアリングの学習歴5ヶ月ほど
  • 静的なWebサイト制作数件。JSはjQueryをコピってぺぐらいのレベル(アルゴリズムちょっとやったぐらい)
  • サーバー?なんか大きそうな機械??
  • SQL…オレ…シッテル(知らない)

案件の概要

  • 別々のECサイトからデータを持ってきて比較したい
  • 1日1回持ってくればいい
  • データはn万件想定
  • 知り合いからの紹介。作ってみて良さそうであれば買い取りたい(どちらかというと買い取りたい)
  • 技術選定や大枠のフォローはあり

技術選定

  • Heroku
  • PostgreSQL
  • Node.js
  • puppeteer

フレームワークは無し

結果

  • 6月中旬から開始で8月頭に納品
  • 納品直後にエラー頻発して2週間ぐらいその対応に追われる
  • 現在は安定運用

システム開発について

実際にコードを書いて指摘を受けた部分、アドバイスされたこと、自分で感じたことを書いてます。

エラーを読む

これができているようで出来ていなかったです。一番良いのは現役のエンジニアにデバッグしてもらうことかと思います。
できればその様子を横で見させてもらうこと(もしくは自分のデバッグ処理を横で見ててもらう)。

思考プロセスが全く違うなと感じました。

僕は起きた事象から「何が起きたか?」を推測して進めようとしてました。
(経験の浅い人はこうする傾向があると思います)
現役の方はエラーが出ているところから順番に確認していきました。

ログを見る

特にシステム開発であれば本番環境で動かすと思うのですが、ログが出力されるようになっていると思います。
(むしろなっていなければ、出すようにした方がいい)

これもエラーと全く同じです。まず、ログを見る。とても大事。

詰まるところが●ぬほど多い。というか●ぬ

調べていったり要件を細かく分けていくと「あれ?これ意外といけんじゃね?」感がでます。
ネットに落ちてるコードちょっと変えれば…と思っていたらめっちゃ詰まります。

(例)Webページに遷移した後スクレイピングする処理
1. ローカル環境で試す
2. Webページにはすぐに遷移できる
3. データの取得がうまくいかない
4. データを取得した時にtextで取得しているので、DOMのツリー構造を保持していない
5. ツリー構造ごと取得する
6. 動作を確認できたので本番環境で試すことにする
7. 本番にデプロイできない
8. デプロイなんとかできても動かない(環境構築ができていない)
9. 環境構築できてもローカルで出ないエラーが発生する
…etc

毎日こんな感じでしたToT

デバッグ処理は変数をダンプすることが多々

詰まったエラーで一番多かったのは、変数にデータを想定通りに渡せていないことでした。
それに気付かずロジックを書き換えたり…非常に無駄な時間を過ごしました。

console.log()で泥臭く1つ1つの変数を確認していくとエラーの原因に行き当たることが多かったです。

変数名 / 関数名は分かり易く

基準は第三者が見た時に何をするのか分かるかどうか?

いろんな人にたくさん言われましたが中々習得できず…
他人のコードを積極的に見たり、コードレビューを重ねて受けるのがいいと思います。

結局一番見た記事は一次情報(公式)

記事を探すときは、大枠で

  • こういうことできるかな?を探す
  • エラーを直接打って対応方法を調べる

が多いと思うのですが、残念ながら記事に便利なものは余り多くはありませんでした。
なぜならシステム開発は応用的なものを作り上げていくので(基礎 + 基礎 = 応用)。
微妙に基礎から変更するにはそのメソッドが「何に対してどう動くのか」という仕組み部分を知る必要がある為に、公式にいってメソッドの処理を細かく見ないといけません。

こういうことできるかな?を探す

これはQiitaなどで書かれていることもありました。
ただ数回の検索ででてこない場合はロジックを考えてメソッドを組み合わせないとできないことが殆どでした。

エラーを直接打って対応方法を調べる

多くの人がぶつかったエラーでない場合は大抵でてきません。そして、自身とは関係ないことが殆ど。
エラーを読んで各メソッドがどういう処理をしているか把握する。変数に何が入っているか確認する。

基礎を把握することが一番の近道でした。

処理の流れを先に書く

今回処理ごとに進めていって最後の段階でロジックが破綻していることに気付いたことがあります…
そんなことがあったり、メンターからのフィードバックも受け現在は処理の流れを先に書くようにしています。

//(例)オセロゲームの場合// mainの処理// イベント(クリック)をリッスンしている// マスをクリックする(ユーザー側の操作)// そのマスが押せるか判定する// 押せる場合は石を置く// それによって裏返る石がないか確認する// 次のターンに移るfunction(turn){ターン数を変更する処理returnturn}function(turn){番手を表記する処理return番手}functionput(turn){(マス)の状態を変更する処理(設置)配列の数値を変更する処理turn(先手後手)によって配列の変更パターンが変わるreturnなし}functionturnOver(put){putした後に発火させる(マス)の状態を変更する処理(裏返り)配列の数値を変更する処理returnなし}function(turnOver){turnOverした後に発火させる設置するところを明示する処理※マス目の配列を3にする}

これは現在書いているオセロゲームの処理ですが、このように日本語で記載していくことが大切だなーと学びました。
結果として時間が短縮できますし、今後必要な実装なども見えてくるので。
(上記のオセロゲームは書きかけのものです)

この記事なんかは見易かったのでおすすめです^^
http://labs.timedia.co.jp/2013/07/write-othello-in-javascript-with-functional-style.html

都度メモをとる

エラーとずっと対峙していると「あれ…?このエラーさっきも出会わなかったっけ?」
みたいな迷子状態が発生します。特に頭も動かしてるのでゴチャゴチャになってます。

都度メモをとること。何でもいいです。

// hogehogeは想定通りfugafugaの値が挿入されていた// HOGEが動いていない可能性がある→確認したが実行はされていた

こんな感じで大丈夫なので自分の思考をメモにとっておいて、自分のキャッシュを軽くすることが大切です。

デバッグ用の一時的なコードなら、その旨をコメントなどで示す

「後で消そう〜」みたいな処理もその旨を記載する。
未来の自分が覚えているとは限らない。

コードコメントは消す

便利なので「とりあえず今の処理はコピーしておいておいて…」は大抵使わない。
積もっていって大抵は醜いコードの塊ができるのでGitを使ってコメントはなるべく使わない。

また、コードの説明もできる限り避ける。
コメント書かなくても読めるようなコードを目指す。

DBの基礎は知っておく

簡単な成績表みたいなもので良いので、SQLでかけるようになっておく。
CRUD処理はできるようになっておく。

puppeteer(スクレイピング)に関すること

puppeteerの処理を実装していて詰まった内容を記載していきます。

スクレイピングはサーバー攻撃になり得る

これはpuppeteerに限らずですが、スクレイピングはサーバー攻撃になり得ます。
スクレイピングではないものの、過去学生の方が悪意なく書いたコードが無限に繰り返される処理で事件になったこともあります。

また、スクレイピングを利用規約にNGと明示しているサイトもあります。
(というか基本的に快くは受け入れられていないはず)

実装前は利用規約を確認すること。
スリープ処理を入れること。

headless: falseについて

ローカルで実装しているときはheadless: falseをオプションにつけて動かすととても見易いですが、本番環境(Heroku上)では使えません(ディスプレイがないので当然)
しかも、最悪なことにheadless: falseの状態だと動かないメソッドもあり、本番環境で苦労したことを覚えています…

早い段階でheadless: tureで動かすことをお勧めします。

実例で言うと

  • click
  • goto

この2つはheadless: falseで動きが変わるので注意が必要です。

goto処理について

おそらくgotoメソッドは不具合を抱えています。
https://stackoverflow.com/questions/62618052/how-to-get-puppeteer-to-simply-load-a-web-page

このStackOverflowにあるGitHubのIssuesも見ましたが、答えが出でてない。

症状としては、

  1. httpリクエストが飛ぶ
  2. レスポンスが返ってこない
  3. タイムアウトエラーになる(この後何度繰り返しても同一URLに弾かれる)

議論の中では、CSS / JSのロードが終わっていないことも書かれていてそれが原因?
と書かれていましたが、CSS / JSのロードを無効にしても同じ症状。

また、質問者は解決したと書いていますが、そのURLへのgoto処理諦めているだけなので根本的には解決していない。gotoできなくなるURLに共通の特徴もない。

自分の場合は1,000件につき10件程度の処理落ちだったので、そのまま無視して実装しています。
(この辺はシステム要件によって対応が変わってきますが、自分の場合は無視しても問題なかったので)

自分は似たような案件がくれば次はseleniumで対応すると思います。

click処理について

page.click処理はheadless: falseでは機能しません。
公式の中でChromiumのバグだ!って以前見たのですが見つけられずorz

ページを次のページに移動させる処理はURLのpage部分に変数を入れてforで回して解決しました。

userAgentについて

こちらも意外な落とし穴だったのですが、headless: falseの状態はuserAgentに「headlessで動いてるで」っていう記載がされています。
そして、サイトによってはheadlessはアクセスを弾くサイトもあるのでご注意を。

その場合はuserAgentを変更するメソッドがあるのでそちらを使用してください。

要素があるかを判断する

constgetProduct=awaitpage.$("className").then((res)=>!!res);if(getProduct){// 要素があった時の処理}

これはとても使えました^^
(元記事があったのですが、見つからなかったので断念してます)

要素が複数ある場合の処理

要素の取得は単数形と複数形があります。
取得するメソッドに応じて取得してくる内容もElementHandleなのかNodeListなのか変わってきます。
こういった細かい違いは探さないと出てこずハマりやすいのでご注意ください。

ちなみにpuppeteerで開発する際はこの2記事がとてもオススメです!
ここでわからなければ大抵公式にしか解答がなかったように思います。

https://qiita.com/taminif/items/1ba7f68aedd68bae5e09
https://qiita.com/go_sagawa/items/85f97deab7ccfdce53ea

DOMのツリー構造を保持したい場合

記事に書いている要素の取得は、DOM取得→パース処理を流れで書いています。
DOMのツリー構造をそのまま保持して兄弟要素などを判定したい場合にそのまま流用してるとハマるのでご注意ください。

自分は一旦空の配列に入れて対応しました。

new Browser

今回自分はn千件ほどの処理を同一ブラウザで処理していたのですが、puppeteerの仕様としてブラウザのキャッシュはずっと保持し続けるようです(というより解放されない)。
ブラウザをnewしたタイミングで作り直されるとのこと。

で、処理落ちを繰り返したので数件ほどでブラウザを立ち上げ直す処理を入れたら改善されました。

並列処理について

map + Promise.allで擬似的に並列処理を実装できます。

async()=>{awaitPromise.all(sampleArr[i].map(async(product)=>{// mapで繰り返したい処理}));}

件数が多い処理を実装するときはおすすめです^^

Heroku(PaaS)に関すること

Herokuで実行する時に詰まった部分です。

サーバーとは何かを理解する

これが理解できていなかったので無茶苦茶詰まりました…
一番おすすめなのはheroku run bashでbashを使用することができるので、
使用した後lspwdコマンドを打つと普段ターミナルで操作しているような感覚で操作できます。
後、デプロイされている内容を視覚的に確認できます。

自身は今回から「サーバーとは何か?」をもっと知っていかないとまずいと考えてlinuxの学習を始めました。
AWS / GCP共に無料でインスタンス生成までできるのでおすすめです^^
(両方公式ドキュメントも豊富ですし、記事も沢山あるので1人でもアプリをデプロイするぐらいまではいけると思います)

無料枠でできること / できないこと

これは最初に明確にしていった方が良かったです…
今回の自分の件であればPostgreSQLは無料枠だと1万レコードが限界でした。

また、メモリの容量や稼働時間などクライアントが知りたがる範囲は事前に把握しておくべきでした。

まず、本番環境で小さい単位で動かしてみる

当然ローカルとは環境が違うので、ビルドする時の障害があったり、ローカルでは動くけど本番で動かないメソッドなどもあります。
Heroku + puppeteerに関して言うと専用のビルドパックが必要です。
これは、過去不具合があったものを修正するために後から出されたのですが、タイミングによっては自分がひっかかることもあり得ます。

なので全部完成させてからではなく、ポイントポイントで確認すると手戻りも少なくなるのかなと思います。

ローカルと同じ環境を構築する

納品後に「ローカルでは動くけど、本番環境で想定通りの振る舞いをしない」という自体が発生ToT

しかも、問題が何個か重なっているので改修にとても時間がかかりました(どうしようもなくなったので、最後は現役エンジニアの方にデバッグ手伝ってもらいました)

この時にDockerを使うと「環境が違うことが原因」or「そもそもコードに問題がある」という問題を切り分けられたので、楽に対応することができます。

(番外編)クレジットカード登録

herokuのような海外のサイトを使うとそこそこの頻度でクレジットカードの登録を求められます。
登録しておくとお金使わなくても使用できるものが変わるとか、そもそも登録しないとサービス使わせませんみたいなサービスもあるかと。

自分はそんなときはバンドルカードを使用しています。
https://vandle.jp/

クレジットカードのように使えますが、実態はプリペイドカードとして運用できるのでチャージ金額以上の支払いは無くせます。
(運用やプランによってはクレジットカードと同じ状態になるのでご注意ください)

年会費、登録費無料で身分証不要&即発行(アプリ)
便利なのでおすすめです^^

さいごに

さいごまで見ていただいてありがとうございました!

今回で学んだことが非常に多くあったので、今後はそれらを消化しながら進んでいきたいと思います。

①デバッグ / コードレビューはお金を支払ってでも現役エンジニアにお願いする
②基礎を学ぶことが遠回りのように見えて近道
③簡単なことはGASで解決できる

一応結論をもう一回載せて締めにさせていただきます。ありがとうございましたm_ _m

Node.jsでGoogle Drive上のファイルをコピーする (Google Drive API v3)

$
0
0

Google Drive上でのファイルコピーを試します。

メソッドはFiles: copyになります。

ライブラリの書き方だとdrive.files.copy()です。

スコープ指定

https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.appdata
https://www.googleapis.com/auth/drive.photos.readonly

Google Drive上のファイルのコピー

チュートリアルfunction listFilesの箇所を書き換えて利用してみます。

機能的にはfunction copyFileとかにリネームした方が良いでしょうが一旦動かす体なのでスルー

folderIdにGoogle Driveのアップロード先のフォルダのIDを指定しましょう。

copy.js
//省略asyncfunctionlistFiles(auth){constdrive=google.drive({version:'v3',auth});constparams={fileId:`xxxxxxxxxxx`//ここにコピーしたいファイルのIDを指定};try{constres=awaitdrive.files.copy(params);console.log(res.data);}catch(error){returnerror;}};

実行すると指定したファイルがGoogle Driveのフォルダ同じフォルダ上でコピーされます。

別のフォルダにコピー

drive.files.copy()をパッとみた感じフォルダのparents指定ができそうでした。

参考: https://github.com/googleapis/google-api-nodejs-client/blob/master/src/apis/drive/v3.ts#L4136-L4189

//省略constparams={fileId:`xxxxxxxxxxx`,//ここにコピーしたいファイルのIDを指定requestBody:{parents:[`xxxxxxxxxxx`]//ここにコピー先のフォルダIDを指定}};//省略

これで元々のフォルダにファイルを残しつつ、別のファイルとして別のフォルダにファイルをコピーすることができます。

よもやま

.update()で複数ペアレントを指定したり、ペアレントを削除したりすることでファイルの移動ができますが、この場合はコピーではなく同じIDのファイルを複数フォルダから参照している状態になります。

今回の.copy()は全く別のファイルを作成する形です。
IDが異なるものができるので扱う際には違いの注意をしましょう。

Node.jsでGoogle Drive上にフォルダ作成と存在確認 (Google Drive API v3)

$
0
0

Google Drive上でフォルダを作成します。

メソッドはFiles: createになります。

ライブラリの書き方だとdrive.files.create()です。

スコープ指定

https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.appdata

Google Drive上で新規フォルダ作成

チュートリアルfunction listFilesの箇所を書き換えて利用してみます。

機能的にはfunction createFolderとかにリネームした方が良いでしょうが一旦動かす体なのでスルー

createFolder.js
//省略asyncfunctionlistFiles(auth){constdrive=google.drive({version:'v3',auth});constfileMetadata={'name':'n0bisuke',//作成したいフォルダの名前'mimeType':'application/vnd.google-apps.folder'};constparams={resource:fileMetadata,fields:'id'}try{constres=awaitdrive.files.create(params);console.log(res.data);}catch(error){console.log(error);}};

マイドライブのルートにn0bisukeというフォルダが作成されます。

フォルダを指定してフォルダ内にフォルダを新規作成

requestBodyにparents,name,mimeTypeを指定して実行します。

//省略asyncfunctionlistFiles(auth){constdrive=google.drive({version:'v3',auth});constparams={fields:'id',requestBody:{name:'n0bisuke',//新規作成するフォルダの名前mimeType:'application/vnd.google-apps.folder'parents:[`xxxxxxxxxxxxxxxxxxxxxxx`],//新規作成したフォルダを置くフォルダのID}}try{constres=awaitdrive.files.create(params);console.log(res.data);}catch(error){console.log(error);}};

応用: 同名のフォルダが存在しなければフォルダを新規作成する

フォルダを自動的に作ったりするときに使いそうなのでメモしておきます。

  1. フォルダの確認
  2. 作成

という2ステップが考えられます。

1. 既に同じ名前のフォルダがあるかどうかの確認

Google Driveだと同じ名前のフォルダを同じ階層に作成することができてしまうので、防ぎたい場合は存在チェックを先にした方が良いと思います。(メソッドがなさそうな気がしたので手動)

FOLDER_IDで指定したフォルダの中を.list()で確認し、n0bisukeという名前のフォルダがあるか無いかをチェックします。

//省略asyncfunctionlistFiles(auth){constnewFolderName=`n0bisuke`;//調べたいフォルダ名constdrive=google.drive({version:'v3',auth});constFOLDER_ID=`xxxxxxxxxxxxxxxx`;//ここにフォルダIDを指定constparams={q:`'${FOLDER_ID}' in parents and trashed = false`,}constres=awaitdrive.files.list(params);constexists=res.data.files.find(file=>file.name===newFolderName);if(exists){console.log(`${newFolderName}は存在します。`);}else{console.log(`${newFolderName}は存在しません。`);}};

2. フォルダの作成まで

まとめて書くとこんな感じです。

//省略asyncfunctionlistFiles(auth){constnewFolderName=`n0bisuke`;//調べたいフォルダ名constFOLDER_ID=`xxxxxxxxxxxxxxxxxxxxxx`;//ここにフォルダIDを指定constdrive=google.drive({version:'v3',auth});constparams={q:`'${FOLDER_ID}' in parents and trashed = false`,}constres=awaitdrive.files.list(params);constexists=res.data.files.find(file=>file.name===newFolderName);if(exists){console.log(`${newFolderName}は存在します。`);}else{console.log(`${newFolderName}は存在しません。`);console.log(`フォルダを新規作成します。`)constparams={fields:'id',requestBody:{parents:[FOLDER_ID],name:newFolderName,mimeType:'application/vnd.google-apps.folder'}}constres=awaitdrive.files.create(params);console.log(res.data);}};

これで特定のフォルダ内にn0bisukeフォルダがあれば何もせず、n0bisukeフォルダが存在しなければ新規作成してくれます。

よもやま

.create()の時に重複を許可しない指定っぽいのができればこんなの周りくどいことしなくてよいのでもしそんな指定の仕方知ってる人いたら教えてください。

Fastify + Typescriptでrequestに任意の情報を加える

$
0
0

背景

認証などをサーバ側で行なった際、ユーザを特定させると思うのですが、その特定したユーザ情報をrequestにいい感じに(Typescriptの怒られない形)で実現したいなと思い調べてみました。

ちなみに、fastifyのversionはv3系です。

結論を先に書いとくと、decorateRequestを使うといい感じにできました。

やり方

認証機能などを実装する際、fastifyのhooksを用いて下記のように書くと思うのですが、そうするとTSに怒られる。

fastify.addHook('onRequest', (request, reply, next) => {
  // TSにrequestにuserIdなんてプロパティないわと怒られる
 request.userId = userId; 
  next()
})

かと言って下記の様にしちゃうと、どこからでも上書き出来てしまって嫌だな〜と思っていた。
できれば、setterを使いたい。

# @types/fastify/index.d.ts
import { FastifyInstance } from 'fastify';

declare module 'fastify' {

  export interface FastifyRequest {
    userId: number; // ここをreadonly userId: numberにしたい
  }
}

そこで、色々調べているとdecorateRequestなるものを使えば、requestに新しいプロパティやメソッドを生やせることがわかったので使ってみることにしました。

定義はこんな感じ。

const server = fastify();

...

server.decorateRequest('setUserInfo', function (this: FastifyInstance, userId: number) {
  this.userId = userId;
});

...

export default server;

すると、addHook時などで、request.setUserInfoでアクセスできる様になってる!!

server.addHook('onRequest', async (request: FastifyRequest) => {
  // 認証・認可の処理をして、そのuserIdを取得する
 const userId = await auth(request);

  request.setUserInfo(userId);
});

型定義はこんな感じ。

# @types/fastify/index.d.ts
import { FastifyInstance } from 'fastify';

export interface setUserInfo {
  (userId: number): void;
}

declare module 'fastify' {
  export interface FastifyInstance {
    userId: number;
  }

  export interface FastifyRequest {
    readonly userId: number;
    setUserInfo: setUserInfo;
  }
}

実際、APIなどの処理部分でrequest.userIdってやると値が取れるようにになっている。

interface IQuerystring {
  name: string;
}

export const handler = async (request: FastifyRequest<{ Querystring: IQuerystring }>, reply: FastifyReply) => {
  ...

  // 認証されたユーザのuserIdがコンソールに出力される。
  console.log(request.userId);

  ...
};

補足

なお、別に最初の

request.userId = userId; 

このケースでも型定義さえ拡張しとけば普通に動くけど、公式

Note: The usage of decorateReply and decorateRequest is optional in this case but will allow Fastify to optimize for performance

って書いてあるから、上記の様に書くなら

server.decorateRequest('userId', null);

みたいな感じで定義しておいた方がいいかも。

結論

fastifyRequestにメソッドとかプロパティをはやしたいなら、decorateRequestを使った方が良さげ。

なお、reply周りをいじりたいときは、decorateReplyがあるみたいですね。

AWS DocumentDB に Node.js/mongoose で接続する

$
0
0

やりたいこと

mongo 互換の AWS DocumentDB に mongoose から接続したい。

mongoose で docker でたてた MongoDB に接続するのにはまった話

以前書いたような感じで接続したい。

今回の環境は、AWS で立てた AmazonLinux2 インスタンスからの接続。

$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2"
ID="amzn"
ID_LIKE="centos rhel fedora"
VERSION_ID="2"
PRETTY_NAME="Amazon Linux 2"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"
HOME_URL="https://amazonlinux.com/"

$ node -v
v14.8.0

なお、DocumentDB は、Tokyo Region でも使える。ちと高い気もするが、EC2 インスタンス上に MongoDB をインストールして、バックアップだなんだと諸々の手間を考えると、個人では遠慮したいところだが、会社ならまぁ良いのでは?という感じ。
DocumentDB のクラスターを立ち上げるためには、インスタンスを1つしか立ち上げないとしても、VPC は Multi AZ 構成になっている必要がある。

DocumentDB の接続方法

特に何も考えずに普通に DocumentDB を立ち上げると、インスタンス情報の Connectivity & Security という項目に、
まずは、pem をダウンロードし、それを使って接続しろと書いてある。

曰く、

Download the Amazon DocumentDB Certificate Authority (CA) certificate required to authenticate to your instance

wget https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem

mongo シェルでこのインスタンスに接続する

mongo --ssl --host xxxx.xxxx.ap-northeast-1.docdb.amazonaws.com:27017 --sslCAFile rds-combined-ca-bundle.pem --username xxxx --password <insertYourPassword>

アプリケーションでこのインスタンスに接続する

mongodb://xxxx:<insertYourPassword>@xxxx.xxxx.ap-northeast-1.docdb.amazonaws.com:27017/?ssl=true&ssl_ca_certs=rds-combined-ca-bundle.pem&retryWrites=false

こんな感じ。

Node アプリから mongoose で接続

Shell を用いての接続はうまくいくけど、node アプリからの接続は、どうしたら良いの?と探してみたところ、stackoverflow に答えがあった。

connection error while connecting to AWS DocumentDB

で、結局こうなった。

index.js
constmongoose=require('mongoose');constfs=require('fs');mongoose.connect('mongodb://xxxx.xxxx.ap-northeast-1.docdb.amazonaws.com:27017',{useNewUrlParser:true,useUnifiedTopology:true,ssl:true,sslValidate:false,sslCA:fs.readFileSync('./rds-combined-ca-bundle.pem'),user:'<userName>',pass:'<password>',dbName:'<dbName>'});constCat=mongoose.model('Cat',{name:String});constkitty=newCat({name:'Zildjian'});kitty.save().then(()=>console.log('meow'));

pem ファイルは、fs で読み込まないと、monngoose 側でうまくやってくれたりはしない。
実行してみる。

$ node index.js
meow

いったっぽい。
Shell から入って確認。

rs0:PRIMARY> show collections
cats
rs0:PRIMARY> db.cats.find()
{ "_id" : ObjectId("5f478c677f7fde2e..."), "name" : "Zildjian", "__v" : 0 }

成功。

余談

MongoDB は、ほぼほぼ JSON な扱いが出来たりなど、アプリ開発者としては、非常に使いやすいが、MySQL などのようにお手軽にインフラ構築が出来ない。し、ちょっと高い。コンテナなどを使えば良いんだろうけど、運用する場合、ただ立ち上がれば良いという話ではないし。
なので、本当に MongoDB でなきゃダメか?は、インフラ担当者と話し合って決めるのが幸せだと思う。

Viewing all 8835 articles
Browse latest View live