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

node.jsでgoogleapiのリダイレクトURIに任意の文字列(state)を追加する方法

$
0
0

この記事について

バックエンドで Google API を使っています。
言語はNode.jsで書いています。
Google APIの使用には、Google APIs Node.js Client
を使っています。

今回は、Google APIのユーザー認証後のリダイレクトURIに任意の文字列を指定する方法を記載します。

事前準備

GCPコンソールにてGoogleプロジェクトを作成し、認証情報を作成し、何かしらのAPIを有効化しておいてください。
↓GCPコンソール(developers)
https://console.developers.google.com/apis

やり方

リダイレクトURLに任意の文字列を含めたい場合は、stateパラメータを認証URLに含めることで可能です。
https://developers.google.com/identity/protocols/oauth2/web-server#request-parameter-state

(ちなみに)stateパラメータの用途

You can use this parameter for several purposes, such as directing the user to the correct resource in your application, sending nonces, and mitigating cross-site request forgery.

日本語訳
stateパラメータは下記のような目的で利用されます。
・ユーザーをアプリの正しいリソースにリダイレクトするため
・乱数を送るため
・CSRF対策のため

実際の認証URL例

公式ドキュメントにも書いてありますが、こんな感じで認証URLにstateを含めます。

https://accounts.google.com/o/oauth2/v2/auth?
 scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly&
 access_type=offline&
 response_type=code&
 state=state_parameter_passthrough_value&
 redirect_uri=https%3A//oauth2.example.com/code&
 client_id=client_id

Node.jsでの実装例

useGoogleApi.js
const{google}=require("googleapis");constoauth2Client=newgoogle.auth.OAuth2(CLIENT_ID,CLIENT_SECRET,REDIRECT_URI);constscopes="https://www.googleapis.com/auth/blogger";constauthUrl=oauth2Client.generateAuthUrl({access_type:"offline",scope:scopes,state:"abcdefghijklmnopqrstuvwxuz",// ←任意の文字列});console.log(authUrl)
useGoogleApi.jsがあるフォルダで下記コマンドを実行
node useGoogleApi.js
するとこんな感じでURLがコンソールに出力されるので、そのURLにアクセスする。
https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fblogger&state=abcdefghijklmnopqrstuvwxuz&response_type=code&client_id=your_client_id&redirect_uri=your_redirect_uri

認証画面にてGoogleアカウントを選択すると
リダイレクトしたURLにちゃんとstateが渡されていました^^


Node.jsの基礎

$
0
0

img01.png

Node.js

イベントループによる並行処理

Node.jsの第一の特徴としてあげられるのは、並行処理をマルチスレッドではなくイベントループによって実現するというものです。イベントループは単一のスレッド(シングルスレッド)で動作するため、マルチスレッドのようなリクエスト数の増大に伴う問題が起きづらくなっています。
 イベントループは実行すべきタスクをキューに積み、これを1つづつ取り出してシングルスレッドで実行していきます。逐次処理のようですが、これで並行処理を実現できるのは、ここでのタスクが一連の処理をI/Oの発生するタイミングを境に分割した物だからです。プログラムはI/O実行時にその完了後のタスクを指定し、実際にI/Oが完了すると指定されたタスクがキューに追加されます。

Ecmascript, Web標準

JavaScriptの言語使用はECMAScript(ECMA Internationalという団体に属する技術いんかいのTC39での議論で標準化する)標準によって規定されます。また、ブラウザ環境におけるJavaScriptのAPI仕様を定めるものとして、Web標準がある。これはHTML, CSS, JavaScriptなどのブラウザ関連の技術について、ブラウザ館での使用を統一するための標準です。

ECMAScript 標準

ECMAScript標準のバージョンは2015年にリリースされたES2015以降、西暦であらわされるようになりました。ES2015はもともとES6として標準化が進められていたが、以前のバージョンとの期間が空きすぎてしまったために年次リリースができるように改められました。この結果、ES6と呼ばれていた物は最終的にES2015としてリリースされ、以降ES2016, ES2017, ES2018,...ES2020と続いている。

javascriptの基本

変数宣言

変数宣言をするには、[const, let]を用います。

  • const: 宣言した変数にには値の再割り当てができない(定数)
  • let: 再割り当てが可能

この両者の特徴として、ブロックのスコープ無ことと、同一スコープないで同じ名前の変数を複数同時に宣言できないことがあげられます。

node.js
>constfoo='foo'undefined>letbar='bar'undefined>foo='foo2'UncaughtTypeError:Assignmenttoconstantvariable.>bar='bar2''bar2'

constとletはES2015で導入されましたが、それまではJavaScriptで変数宣言する際は"var"を使用していました。今でも使用可能ですが、すこー部が関数や同じなあ絵の変数を繰り返し宣言可能なことなど、わかりづらく不具合の原因となるので使用しない方が良い。

関数

javascriptで関数を定義する方法はいくつかあります。

  • 名前をつけて宣言する(他の言語と同様に定義)
>functionadd1(a,b){returna+b}undefined
  • 関数式で関数を生成
>constadd2=function(a,b){returna+b}undefined
  • 関数式でも関数に名前をつけられる
>constadd3=functionaddFn(a,b){returna+b}undefined
  • アロー関数での定義(ES2015で導入)
// アロー関数式>constadd4=(a,b)=>{returna+b}undefined// アロー関数式の省力記法({}を省略するとreturnなしで値が返される)>constadd5=(a,b)=>a+bundefined// パラメータが一つならパラメータを囲む()も省力可能>constaddOne=a=>a+1undefined

関数宣言で作る関数はモジュール内でホスティングされ。宣言前に参照できるが、関数式で作成する関すは作る前に参照するとエラーになります。

console.log(add6(1,2))console.log(add7(1,2))functionadd6(a,b){returna+b}constadd7=(a,b)=>a+b>>>// 関数宣言で作成したadd6は宣言前に参照可能3// 関数式で作成したadd7は作る前に参照するとエラーになるUncaughtReferenceError:Cannotaccess'add7'beforeinitialization

オブジェクト

今回は、オブジェクトリテラルから生成されるようなプレーンなオブジェクトについて説明します。

// オブジェクトリテラルによるプレーンなオブジェクトの生成>constobj1={propA:1,propB:2}undefined// プロパティ名を指定して取得(ドット記法)>obj1.propA1// プロパティ名を指定して取得(ブランケット記法)>obj1['propA']1// プロパティの追加>obj1.propC=33>obj1{propA:1,propB:2,propC:3}// プロパティの削除>deleteobj1.propCtrue>obj1{propA:1,propB:2}

プロパティの追加、削除をイミュータブルに行うには、スプレッド構文、レスト構文に使います。

'use strict'constprint=console.logconstobj1={propA:1,propB:2}// スプレッド構文でexecurteを追加constobj2={...obj1,propC:3}// 元のオブジェクトは不変print('obj1',obj1)// 新しく生成されたオブジェクトにキーが追加されるprint('obj2',obj2)#レスト構文でexecuteを削除const{propA,...obj3}=obj2// 元のオブジェクトは不変print('obj2',obj2)// 新しく生成されたオブジェクトにキーが追加されるprint('obj3',obj3)>>>obj1{propA:1,propB:2}obj2{propA:1,propB:2,propC:3}obj2{propA:1,propB:2,propC:3}obj3{propB:2,propC:3}

プロパティの値を取得、設定する際に関数を実行するgetter, setterという機能もあります。

'use strict'constprint=console.logconstprice={value:100,getwithTax(){returnMath.floor(this.value*1.1)},setwithTax(withTax){this.value=Math.ceil(withTax/1.1)}}// getterから値を取得print(price.withTax)// >>> 110// setterで値を設定price.withTax=333// getterで取得される値が更新されることを確認print(price.withTax)// >>> 333// setterにより正しく値が設定されることを確認print(price.value)// >>> 303

配列

配列リテラルで初期化します。

'use strict'constprint=console.log// 配列リテラルで初期化constarr1=['foo','bar']print('arr1.length',arr1.length)// arr1.length 2// 指定したインデックス要素を取得print('arr1[1]',arr1[1])// arr1[1] bar// 指定した要素のインデックスを取得(存在しない場合:-1)print(arr1.indexOf('bar'))// 1// 要素が配列に含まれるかどうかprint(arr1.includes('bar'))// true// 前要素を引数に指定した文字列を結合print(arr1.join('-'))// foo-bar

配列の末尾への要素の追加、削除はそれぞれ push(), pop()を使います。同様に配列の先頭への要素の追加、削除はそれぞれunshift(), shift()を使います。

'use strict'constprint=console.logconstarr1=['foo','bar']// 末尾に要素を追加arr1.push('baz')print('arr1',arr1)// arr1 [ 'foo', 'bar', 'baz' ]// 末尾に要素を複数追加arr1.push('a','b','c')print('arr1',arr1)// arr1 [ 'foo', 'bar', 'baz', 'a', 'b', 'c' ]// 末尾の要素を削除arr1.pop()print(arr1)// [ 'foo', 'bar', 'baz', 'a', 'b' ]// 先頭に要素を追加arr1.unshift('qux')print(arr1)// [ 'qux', 'foo', 'bar', 'baz', 'a', 'b' ]// 先頭の要素を複数追加arr1.unshift('d','e','f')print(arr1)/*
[
  'd',   'e',   'f',
  'qux', 'foo', 'bar',
  'baz', 'a',   'b'
]
*/// 先頭要素を削除arr1.shift()print(arr1)/*
[
  'e',   'f',   'qux',
  'foo', 'bar', 'baz',
  'a',   'b'
]
*/

要素の追加、削除をイミュータブルに行うには、オブジェクトの場合と同様スプレッド構文、レスト構文を使います。

'use strict'constprint=console.logconstarr2=['foo','bar','baz']// スプレッド構文で先頭と末尾に要素を追加constarr3=['a',...arr2,'b','c']print('arr3',arr3)// arr3 [ 'a', 'foo', 'bar', 'baz', 'b', 'c' ]// 末尾の配列はそのままprint('arr2',arr2)// arr2 [ 'foo', 'bar', 'baz' ]// レスト構文で先頭の要素を削除const[head1,head2,...arr4]=arr2print('arr4',arr4)// arr4 [ 'baz' ]// 末尾の配列はそのままprint('arr2',arr2)// arr2 [ 'foo', 'bar', 'baz' ]

配列のレスト構文では、レスト要素が配列の最後でならなければならないという制約があります。配列から要素を削除する際に、これが不都合になる場合はレスト構文の代わりに配列の部分配列を返す'slice()'というメソッドを使います。

'use strict'constprint=console.logconstarr2=['foo','bar','baz']// 切り出したい最初の要素のインデックスと最後の要素の次のインデックスを指定して部分配列を取得するprint(arr2.slice(0,2))// [ 'foo', 'bar' ]// インデックスに負の値を指定すると配列の最後から数えたいんでくすとして扱われるprint(arr2.slice(0,-1))// [ 'foo', 'bar' ]// 第2引数を小着すると、配列の最後まで切り出す意味になるprint(arr2.slice(2))// [ 'baz' ]// 第一引数も第2引数も省略すると、配列の最初から最後までコピーするprint(arr2.slice())// [ 'foo', 'bar', 'baz' ]

配列に対する反服処理はfor文またはfor...of文で記述できます。

'use strict'constprint=console.logconstarr2=['foo','bar','baz']for(consteofarr2){print(e)}>>>foobarbaz

forやfor...ofを使わずに配列の持つメソッドを使って反復処理を実行すつ処理もあります。そのようなメソッドは引数として反復処理の内容を記述した完酢を受け取り、その処理を各要素に適用します。どのメソッドもイミュータブルで、元の配列を変更しません。また、全てのメソッドでコールバック関数が受け取るパラメータは共通で、配列の要素、その要素のインデックス、捜査対象の配列の3つをこの順番で受け取ります。

'use strict'constarr2=['foo','bar','baz']// forEach(): 各要素にコールバック関数の処理を適用し、戻り値はないarr2.forEach(console.log)// map(): 各要素をコールバック関数の戻り値に置き換えた配列を返すconsole.log(arr2.map(e=>e+e))// filter(): コールバック関数が真の値を返す要素の身を含む配列を返すarr2.filter(e=>e.startsWith('b'))// finc(): コールバック関数が真の値を返す最初の要素を返すarr2.find(e=>e.startsWith('b'))>>>foo0['foo','bar','baz']bar1['foo','bar','baz']baz2['foo','bar','baz']['foofoo','barbar','bazbaz']

find()はコールバック関数が一度でもtrueを返せば結果が確定するため、その時点で反復処理を終了します。

クラス

JavaScriptのクラスは、オブジェクト思考言語の経験のある方なら直感的に理解できると思います。privateなメンバーは名前の先頭に#をつけるという、特殊になっています。

'use strict'classFoo{// privateフィールド#privateField=1// publicフィールドpublicField=2// staticなprivateフィールドstatic#staticPrivateField=3// staticなpublicフィールドstaticstaticPublicField=4constructor(parameter){this.filedInitializedInConstructor=parameterconsole.log('Foo constructor')}// privateなgetterget#computed(){returnthis.publicField*2}// privateなsettergetcomputed(){returnthis.#computed}set#computed(value){this.publicField=value/2}setcomputed(value){this.#computed=value}#privateMethod(){returnthis.#privateField}publicMethod(){returnthis.#privateField}static#staticPrivateMethod(){returnthis.#privateField}staticstaticPublicMethod(){returnthis.staticPublicField}}
constfooInstance=newFoo(100)console.log(fooInstance.#privateField)// SyntaxError: Private field '#privateField' must be declared in an enclosing classconsole.log(fooInstance.publicField)// 2  console.log(fooInstance.filedInitializedInConstructor)// 100console.log(fooInstance.#compute)// SyntaxError: Private field '#compute' must be declared in an enclosing classconsole.log(fooInstance.computed)// 4fooInstance.#computed=10// SyntaxError: Private field '#computed' must be declared in an enclosing classfooInstance.computed=10console.log(fooInstance.computed)// 10console.log(fooInstance.publicMethod())// 1console.log(Foo.#staticPrivateField)// SyntaxError: Private field '#staticPrivateField' must be declared in an enclosing classconsole.log(Foo.staticPublicMethod())// 4

継承

classBarextendsFoo{constructor(parameter){super(parameter)this.subClassPublicField=100console.log('Bar constructor')}publicMethod(){returnsuper.publicMethod()*this.subClassPublicField}}constbarInstance=newBar(100)console.log(barInstance.publicField)// 2console.log(barInstance.subClassPublicField)// 100console.log(barInstance.subClassMethod())// TypeError: barInstance.subClassMethod is not a functionconsole.log(Bar.staticPublicField)// 4console.log(Bar.staticPublicMethod())// 4

等価性

JavaScriptには等価性の評価のための演算子として"==="と"=="が存在しますが、常に"==="を使うようにしてください。"==="による等価性の評価は厳密で結果が容易に予測できますが、"=="はそうではないため、利用するとコードの可読性を著しく下げてしまう。

0===''// > false0==''// > true
// プリミティブの場合、同じリテラルで表現されるあたい同士の場合はtrueになる1===1// > true// オブジェクト同士を比較する場合は構造が同じだけで別々のオブジェクトなので、falseになる{'foo':1}==={'foo':1}// > false

commonJs モジュール

modele.ecportsとrequire()

CommonJSモジュールを使う場合、それぞれのJavaScriptファイルが個別のCommonJSモジュールとして扱われます。CommonJSモジュールは、モジュールレベルのスコープでNode.jsが自動的に割り当てるmoduleという変数のexportsプロパティ(module.exports)を通して、外部に関数や変数を後悔します。一方外部モジュールをロードする際には同じくモジュールスコープで割り当てられるrequire()という関数を使います。

math.js
module.exports.add=(a,b)=>a+bmodule.exports.subscribe=(a,b)=>a-b
index.js
constmath=require('./math')constresult=math.add(1,2)console.log(result)>>>3

filenameとdirname

  • __filename: ファイル名の絶対パス
  • __dirname: ディレクトリ名の絶対パス

strictモード

ファイルの先頭に 'use strict'と記述すると、そのファイルないでstrictモードが有効になります。strictモードは安全でパフォーマンスに優れたコードの記述を促すためES5で導入されたJavaScriptのモードです。特徴としてはいかがあげられる。

  • 無効な処理や意図せぬ結果を引き起こしうる処理をエラーにする
  • JavaScriptエンジンによる最適化を阻害する機能を制限または向こうにする
  • ECMAScriptの新しいバージョンへの移行を容易にするため、将来使われうる識別子を予約語として変数等の名前に使えないようにし、将来違う意味を持ちうる構文をエラーにする。
'use strict'letmyString='あいうえお'mystring='かきくけこ'console.log*mystring>>>mystring='かきくけこ'ReferenceError:mystringisnotdefined

mongoDBで複数行をインクリメントしたい時

$
0
0

今回は、1つのステートメントで1つのドキュメントに対して複数のmongo更新をする話について紹介します。
(mongo2.4環境)

例えば、カラムaとカラムbの複数行のレコードをinc値だけ増やしたい時。

db.test.update({"name":"sample_data"}, 
    {$inc:{a:inc}, $inc:{b:inc});

このようにして書くと、bの数値だけ増加します。
複数$incを付けた場合、最後のステートメントだけ反映される仕様のようです。
以下のようにして解決します。

db.test.update({"name":"sample_data"}, 
    {$inc:{a:inc, b:inc});

これで、aとb両方の数値がインクリメントされます。

なお、「この値は増加させて、このフィールドをセットして…」と、$inc$setを組み合わせる場合もあるかと思います。
その場合は、ドキュメントの異なる部分に影響を与える場合は、複数の更新を組み合わせることができます

db.test.update({"name":"sample_data"}, 
    {$inc:{a:inc, b:inc}, $set{c:"test"});

上記は可能ですが、

db.test.update({"name":"sample_data"}, 
    {$inc:{a:inc, b:inc}, $set{b:30});

このような競合はNG,という話。
競合すると、have conflicting mods in updateというエラーが出るかもしれません。

Node.jsとElectronを使った WebAPIで画像を取得して一定時間ごとに画像が切り替わるデスクトップアプリ

$
0
0

デスクトップアプリを作りたかった

Node.jsとElectronを使ってデスクトップアプリを作成しました。
そして、今回はデスクトップアプリ上にWebAPIを使って犬の画像を取得して一定時間ごとに切り替わって表示するようにしました。
image.png

環境

Node.js : v10.16.0

Electron : v11.2.1

ソースコード

ソースコード自体は以下のリンクに載せています。

https://github.com/r-kuno-zer0/DogClock

環境構築方法

今回作成するにあたり以下のサイトの構築方法を参考にさせて頂きました。
環境構築方法(Electronのインストール方法等)についても参考にさせて頂きました。
そのため今回の環境構築については割愛させていただきます。

https://qiita.com/y-tsutsu/items/179717ecbdcc27509e5a

ソースコードについての解説

今回のソースコードについてはjsファイルのみコードの解説を書きたいと思います。特に自分がどうしたらいいか困ったところについて記載をしようと思います。
間違っているところなどもあると思います。その際はコメントなどをしていただけると幸いです。

まずデスクトップアプリを起動するためのmain.jsについて

main.js
const{app,Menu,BrowserWindow}=require('electron');constpath=require('path');consturl=require('url');consthttps=require('https');letmainWindow;//参考にしたサイト https://www.electronjs.org/docs/tutorial/quick-startfunctioncreateWindow(){constwin=newBrowserWindow({//*1width:800,height:600,webPreferences:{nodeIntegration:true}})Menu.setApplicationMenu(null);//*2win.loadFile('index.html')//*3}//参考にしたサイト https://qiita.com/y-tsutsu/items/179717ecbdcc27509e5aapp.on('ready',createWindow);app.on('window-all-closed',()=>{if(process.platform!=='darwin'){app.quit();}});app.on('activate',()=>{if(mainWindow===null){createWindow();}})

このソースコードではデスクトップアプリを起動するためのソースコードです。
*1において作成されるデスクトップのサイズなどを指定します。
*2においてデスクトップにおけるメニュー画面を表示しないようしています。この部分をなくすことによってデスクトップのメニューを出すことが出来ます。
*3において表示したデスクトップにおいて表示するHTMLファイルの指定をします。

犬の画像をWebAPIを利用して一定時間ごとにアクセスして画像を取得して表示するコードです。

webAPI.js
consthttps=require('https');//関数にして一定時間ごとにWebAPIを叩いて更新するようにした。functionGetDog(){//ここからwebAPI 叩いて表示 API https://dog.ceo/dog-api/constreq=https.request("https://dog.ceo/api/breeds/image/random",(res)=>{res.on('data',(chunk)=>{//JSON形式で受けとりparseする    obj=JSON.parse(chunk); //*1//受け取ったJSONの中の画像の部分messageを<img>タグで直にhtmlを書き換えdocument.getElementById("webAPI").innerHTML="<img src="+obj.message+" width = "+300+" height = "+300+"></img>";//*2});res.on('end',()=>{});})req.on('error',(e)=>{console.error(e.message);})req.end();}//このスクリプトのなかの関数を10000ミリ秒ごとに叩くsetInterval(GetDog,10000);//*3//参考にしたサイト https://qiita.com/r-yanyo/items/3ef153dac12e69a2c46c

*1についてはchunkとして受け取ったデータをJSONパーサーにかけてobjに代入します。
obj.messageが犬の画像へのリンクとなっています。
*2についてはHTMLの中のwebAPIのIDのところに直接取得した画像のリンク利用してデスクトップアプリに表示しているHTMLの直接書き換えをしています。
ちなみにHTML側は下記のindex.htmlのようになっています。
*3については10000ミリ秒ごとにGetDogの関数を起動していますここに関してはwebAPIの規約などを読んで書き換えてください(アクセスのし過ぎなどに注意のため)

index.html
//HTML最初部分省略
    <body>
       //時計部分省略
        <div><spanid = "webAPI"></span>//この部分をwebAPI.jsで書き込みをしている
        </div></body>

それ以外の時計のjs, HTML, CSSファイルについては以下のサイトのコードを利用して、自分なりに変更したため以下のサイトを参考にしてください。
https://qiita.com/ukkz/items/07d69c83be1112f10c29

実行方法とファイルの構成

DeskTopAppの下にsrcというディレクトリを作成し、その中にHTML,CSS,jsファイルを置いています。
image.png

実行方法

実際にデスクトップアプリを起動する実行方法

このコマンドを撃つことでデスクトップアプリが表示されます。

npx electron src

今後について

今後犬だけでなく猫のwebAPIを使ったものやPixivやPinterestのAPIを利用して自分の好きな画像を表示することの出来るデスクトップ時計アプリの作成方法などについても作成していきたいと考えています。

参考にさせて頂いたサイト

Electronのインストールなどの参考にさせて頂いたサイト
https://qiita.com/y-tsutsu/items/179717ecbdcc27509e5a
時計を作成する際のソースコードの参考にさせて頂いたサイト
https://qiita.com/ukkz/items/07d69c83be1112f10c29
Electronのクイックスタート(Electronの参考になるサイト)
https://www.electronjs.org/docs/tutorial/quick-start
犬の画像を取得する際に参考にさせて頂いたサイト
https://qiita.com/r-yanyo/items/3ef153dac12e69a2c46c
今回利用させていただいたDOG API
https://dog.ceo/dog-api/

rails6を使おうとしたら、Webpackerがインストールできない。

$
0
0

Rails6使いたい

rails6を使ってシステムを使おうとすると、
Webpackerがインストールできない事態に遭遇しました。

rails webpacker:install

してくださいと言われたので、してみると...

sh: /usr/local/heroku/bin/node: cannot execute binary file
sh: nodejs: command not found
Node.js not installed. Please download and install Node.js https://nodejs.org/en/download/

と表示されインストールできません。

Node.jsをサイトからインストールしたり、

alias nodejs='node'

というエイリアスも追加してみましたが、変化はありませんでした。

解決策

sh: /usr/local/heroku/bin/node: cannot execute binary file

上のエラー文がどうやら関係していたようで

rm -rf /usr/local/heroku /usr/local/lib/heroku /usr/local/bin/heroku ~/.local/share/heroku ~/Library/Caches/heroku

上記のコマンドでherokuのアンインストールを試してから
もう一度

rails webpacker:install

してみると

Webpacker successfully installed 🎉 🍰

と表示され見事Webpackerがインストールできました!

Node.jsからDynamoDBのテーブルの作成・削除と行追加・読み取り

$
0
0

表題の通りNode.jsからDynamoDBのテーブルの作成・削除と行追加・読み取りをメモします。
動作確認した後Qiita掲載用に少々手を入れているのでもしかしたら動かないかもしれません。

テーブルの作成

プライマリキーに「accessKey」を指定する場合

constAWS=require("aws-sdk");AWS.config.update({region:"ap-northeast-1"});// DynamoDB Local使うならendpointの指定が必要constdynamo_opt={apiVersion:'2012-08-10',endpoint:`http://localhost:8000`};constdynamodb=newAWS.DynamoDB(dynamo_opt);constdynamoCreateParams={TableName:"テーブル名",KeySchema:[{AttributeName:"accessKey",KeyType:"HASH"},],AttributeDefinitions:[{AttributeName:"accessKey",AttributeType:"S"}],ProvisionedThroughput:{ReadCapacityUnits:10,WriteCapacityUnits:10}};dynamodb.createTable(dynamoCreateParams,function(err,data){if(err){console.error("Unable to create table. Error JSON:",JSON.stringify(err));}else{console.log("Created table. Table description JSON:",JSON.stringify(data));}});

参考: テーブルの作成 - Amazon DynamoDB

行追加

プライマリキーのaccessKeyに加えuserIdも含めた行を3行追加する。

costtableName="テーブル名";vardynamoDataParams=[{TableName:tableName,Item:{"accessKey":"hoge","userId":1}},{TableName:tableName,Item:{"accessKey":"hage","userId":2}},{TableName:tableName,Item:{"accessKey":"fuga","userId":3}}];varddbClient=newAWS.DynamoDB.DocumentClient(dynamo_opt)varpromises=[];for(leti=0;i<dynamoDataParams.length;i++){(function(i){promises.push(ddbClient.put(dynamoDataParams[i]).promise());})(i);}Promise.all(promises).then(function(){console.log('done');})

行の読み取り

constdynamo_opt={apiVersion:'2012-08-10',endpoint:`http://localhost:8000`};constdynamo_params={TableName:"テーブル名",KeyConditionExpression:"accessKey = :a",ExpressionAttributeValues:{":a":"検索文字列"}};constddb=newAWS.DynamoDB.DocumentClient(dynamo_opt);ddb.query(dynamo_params,function(err,data){if(err){console.error('Unable to query:'+JSON.stringify(err));}else{console.log(JSON.stringify(data));}});

テーブルの削除

constAWS=require("aws-sdk");AWS.config.update({region:"ap-northeast-1"});// DynamoDB Local使うならendpointの指定が必要constdynamo_opt={apiVersion:'2012-08-10',endpoint:`http://localhost:8000`};constdynamodb=newAWS.DynamoDB(dynamo_opt);dynamodb.deleteTable({TableName:tableName},function(err,data){if(err){console.log("Unable to Delete table. Error JSON:",JSON.stringify(err));}else{console.log("Created table. Table description JSON:",JSON.stringify(data));}});

参考: テーブルの削除 - Amazon DynamoDB

Gatsby.jsで無限スクロールを実装する方法

$
0
0

この記事について

無限スクロールしたいというのは、割と良くあることだと思います。
純粋なReactだと無限スクロールのコンポーネントはGithubにもたくさん上がっていて、それらを導入するだけで済むのですが、Gatsby.jsを使用する場合にはちょっとした注意点と対策が必要なので、それを書きます。

まず素直に書いてみる

無限スクロールのコンポーネントには、スター数が多いankeetmaini/react-infinite-scroll-componentを使います。

importReact,{useState}from"react"import{graphql}from"gatsby"importInfiniteScrollfrom'react-infinite-scroll-component'constIndexPage=({data:{allVideo}})=>{const[showVideoIndex,setShowVideoIndex]=useState(24)const[showVideos,setShowVideos]=useState(allVideo.nodes)return(<InfiniteScrolldataLength={showVideoIndex}next={()=>setShowVideoIndex(showVideoIndex+24)}hasMore={showVideos.length>showVideoIndex}>{showVideos.slice(0,showVideoIndex).map((video,key)=>(<VideoCardvideo={video}/>))}</InfiniteScroll></div>
  )
}

export default IndexPage

export const query = graphql`
{allVideo{nodes{idthumbnail_image{childImageSharp{fluid(maxWidth:300){...GatsbyImageSharpFluid}}
            }
        }
    }
}
`

これは、動画のサムネイルを表示して、無限スクロールする例です。
簡単にやっていることを説明すると、allVideoというgraphQLを発行し、全ての動画を配列で取得、スクロールするごとにshowVideoIndexを加算し、allVideo配列の最初からshowVideoIndexまでのサムネイルを表示しています。

問題点

上記のプログラムには、問題があります。
動画が増えれば増えるほど、このページは読み込みが重くなります。
さらに、上記のページに全てのページからワンクリックでアクセスできる場合、サイト全体が重くなります。

理由は、Gatsby.jsがバックグラウンドで読ませているpage-data.jsonにあります。

Gatsby.jsは、まず初期レンダリングの状態を静的なHTMLとして生成します。上記の例だと、初期のshowVideoIndexは24なので、24個の動画サムネイルが表示される状態のHTMLファイルを生成します。

それと同時に、上記のページで発行しているgraphQLの結果をpage-data.jsonとして生成しています。
HTMLファイルと一緒にブラウザに渡して、Hydrateしているみたいです。

page-data.jsonにはクエリ結果が1つのファイルに収められているので、動画数が10000とかになれば、数MBのファイルになるでしょう。

さらに、Gatsbyは、Gatsby Link(<Link/>)先のページのpage-data.jsonもプリフェッチします。
先ほど全てのページが重くなる可能性があると言ったのは、そのためです。

じゃあapiを作って、そこから読み込むというのが選択肢になるわけですが、外部のAPIを使った場合、Gatsbyの生成した画像URLなどは参照できないわけです。

解決策

一度に読み込む分のデータを切り分けて、jsonファイルにします。
JSON Outputというめちゃめちゃ便利なGatsbyプラグインがあるので、これを使います。
JSON Outputをインストールしたら、gatsby-config.jsのpluginsをこんな感じで書き換えます。

// あなたのサイトのURLconstsiteUrl=`https://example.com`module.exports={siteMetadata:{...},plugins:[{resolve:`gatsby-plugin-json-output`,options:{siteUrl:siteUrl,graphQLQuery:`
        {
          allVideo(skip: 24, limit: 240) {
            nodes {
              id
              release_date
              is_original_music
              custom_music_name
              thumbnail_image {
                  childImageSharp {
                      fluid {
                        aspectRatio
                        src
                        srcSet
                        sizes
                      }
                  }
              }
            }
          }
        }`,serialize:results=>results.data.allVideoOrderByReleasedAt.nodes.map(_=>({path:``,// ファイルを生成する先のパス})),serializeFeed:results=>results.data.allVideo.nodes,feedFilename:`all_video`,nodesPerFeedFile:24,// 一回に読み込む数}},],}

これで、jsonファイルが/all_video-1.json/all_video-2.json...みたいな感じで生成されます。

allVideo(skip: 24, limit: 240)としているのは、最初のプログラムで書いた通り、24個の動画を初期レンダリングさせるつもりなので、最初の24個はjsonデータを生成しなくて良いので、スキップさせています。

nodesPerFeedFileは、jsonファイル一つにつき、いくつの動画データを入れるかという意味です。無限スクロール一回につき、いくつの動画を読み込むかという意味にもなります。

なので、無限スクロールできる回数は、ここのskiplimitと、nodesPerFeedFileの値で決まります。(limit - skip) / nodesPerFeedFile = 無限スクロール可能回数です。余りが出る可能性がある場合は、例外が発生しないように気をつけてください。

注意点1

gatsby-config.jsでは、GraphQLのフラグメントが使えません。...GatsbyImageSharpFluidとかです。

Gatsby.jsのGraphQLのフラグメントの中身はgatsby-transformer-sharp/src/fragments.jsに書いてあるので、上記のように中身を直接書きましょう。

base64というのは、画像が読み込まれるまでに表示されるモヤモヤっとしたモザイク画像みたいなのを表示するためのものですが、jsonファイルのサイズが2倍近くになる可能性があるので、いらないと思います。

注意点2

ここで設定したjsonファイル生成は、gatsby developでは機能しません。gatsby buildで生成してください。
一回gatsby buildで生成してしまえば、gatsby developでフェッチできます。

最初のファイルを書き換える

こんな感じにします。

importReact,{useState}from"react"import{graphql}from"gatsby"importInfiniteScrollfrom'react-infinite-scroll-component'constIndexPage=({data:{allVideo}})=>{const[page,setPage]=useState(0)const[videos,setVideos]=useState(allVideo.nodes)const[hasMore,setHasMore]=useState(true)constaddVideos=(newVideos)=>{constvideosCopy=JSON.parse(JSON.stringify(videos))videosCopy.push(...newVideos)setVideos(videosCopy)}constfetchVideos=(nextPage)=>{fetch(`/all_video-${nextPage}.json`).then(response=>response.json()).then(json=>addVideos(json.items)).then(()=>setPage(nextPage)).catch(e=>{// 取得しようとしたjsonファイルが存在しなかった → もう追加データがないsetHasMore(true)console.log(e)})}return(<divclassName='w-full'><InfiniteScrolldataLength={videos.length}next={()=>fetchVideos(page+1)}hasMore={hasMore}>{videos.map((video,key)=>(<VideoCardvideo={video}/>))}</InfiniteScroll></div>)}exportdefaultIndexPageexportconstquery=graphql`
{
    # limitはgatsby-config.jsに依存
    allVideo(limit: 24) {
        nodes {
            id
            thumbnail_image {
                childImageSharp {
                    fluid {
                        ...GatsbyImageSharpFluid
                    }
                }
            }
        }
    }
}
`

スクロールされたら、スクロール回数に応じたjsonファイルをフェッチし、フェッチできなかったら(catchされたら)hasMore=falseにしちゃう感じです。

graphQLのlimit数がgatsby-config.jsに依存しちゃうので、注意しましょう。(ここのlimit数が10でgatsby-config.jsのskip数が20とかになると、11個目〜20個目が表示されません。)

これでどれだけ動画数が増えても重くなりませんし、Gatsbyによって生成された画像などを無限スクロールで表示することができます。

JavaScript でコンストラクタをプライベートにする

$
0
0

はじめに

  • コンストラクタを不可視にしたい
  • 即時関数でもできるが、読みづらいので他の方法を考えてみた
  • 可視性の境界をモジュール単位にするのがシンプルっぽい

やりかた

インスタンスを生成する関数だけエクスポートする。

Point.js
classPoint{constructor(x,y){this.x=xthis.y=y}}// ファクトリーメソッドfunctionpointAt(x,y){returnnewPoint(x,y)}// ファクトリーメソッドだけ export するexport{pointAt}
利用側
import{pointAt}from'./Point.js'letp=pointAt(0,1)

使いどき

インスタンスの生成方法を強制したいとき。
Java の Integer クラスのように、頻繁に使われれる値オブジェクトをキャッシュするときなど。

2次元平面上の座標を表す Pointクラスを考えます。xyの値を保持するだけのクラスです。同値判定を簡単に行うため、同じ座標の場合は常に同じオブジェクトの参照を返すようにしています。

Point.js
classPoint{constructor(x,y){this.x=xthis.y=y}}// インスタンスを格納しておくマップconstcache={}// ファクトリーメソッド// 同じ x, y の組み合わせは常に同じインスタンスを返すfunctionpointAt(x,y){letkey=x+':'+yif(keyincache){returncache[key]}else{letinstance=newPoint(x,y)cache[key]=instancereturninstance}}export{pointAt}
利用側
import{pointAt}from'./Point.js'letp1=pointAt(1,2)letp2=pointAt(1,2)p1===p2// これが true を返すので、プリミティブな値のように扱える

Array.includesは参照での比較を行うためプリミティブな値でしか使えませんが、このファクトリーメソッドで生成した Pointは同じ値は同じインスタンスなのでそのまま使えます。

利用例
import{pointAt}from'./Point.js'letarr=[pointAt(0,1),pointAt(0,2)]lettarget=pointAt(0,2)arr.includes(target)// true を返す

MyAnimListのアニメランキングをグラフ入りMDにする

$
0
0

使用例

個人的に、はてなブログで使っています。投稿する部分は割愛。

レポジトリ

https://github.com/and0ry0/myanimelist-email

前提

package.json
"dependencies":{"json2md":"^1.9.2","node-fetch":"^2.6.1",}

アニメオブジェクト

index.js
constfetch=require('node-fetch')functionconvertAnime(anime){constjaTitle=anime.title.replace('Shingeki no Kyojin','進撃の巨人')// 日本語翻訳適当ですいませんreturn{rank:anime.rank,url:anime.url,title:jaTitle,score:anime.score,start:anime.start_date,image_url:anime.image_url,members:anime.members}}

MDに変換

index.js
constconvertToMd=(anime)=>{return[{h2:anime.rank+'. '+anime.title+' ('+anime.members.toLocaleString('ja-JP')+'人視聴)'},{link:{title:'MyAnimeListで詳細を見る',source:anime.url}},{addImage:{title:'MyAnimeListのサムネ',source:anime.image_url}}{ul:['視聴者数: **'+anime.members.toLocaleString('ja-JP')+'人**','スコア: **'+anime.score,'放送開始時期: '+anime.start,]},{p:''/* 改行です */}]}

Jikan APIでランキング取得

詳しくはJikanのDocsを参照。

index.js
// Get anime rankingasyncfunctionMalRank(){// Jikan API https://jikan.docs.apiary.io/constres=awaitfetch('https://api.jikan.moe/v3/top/anime/1/bypopularity')constdata=awaitres.json()// アニメの配列を作るconsttopAnimes=data.top.map((anime)=>convertAnime(anime))constfirstAnime={title:topAnimes[0].title,members:topAnimes[0].members}constjson2md=require('json2md')// 画像とグラフを用意するjson2md.converters.addImage=function({title,source}){return'<img width="150px" title="'+title+'" src="'+source+'" />'}json2md.converters.animeGraph=function({rank,title,members}){// 相対的に長さを作るconstrelativeWidth=(members-topAnimes[10].members)/(firstAnime.members-topAnimes[10].members)return'<div class="graphBox"><div style="width: '+relativeWidth*100+'%;" class="title">'+rank+'. '+title+'</div><div class="members">'+members.toLocaleString('ja-JP')+'</div></div>'}// はてなブログ対策のspan https://blog.uchiten.info/entry/2017/01/30/174500conststyle=`<span></span><style>.graphBox{width:100%;display:flex;padding:.3em;margin:0 0 .2em;position:relative}.graphBox>.title{overflow:visible;white-space:nowrap;background:#add8e6;font-weight:700;padding:.3em}.graphBox>.members{color:gray;position:absolute;right:.3em;padding:.3em}</style></span>\n\n`constgraphMd=`## Top10はこんな感じ \n\n`+json2md(graphJson)+`\n\n11位のアニメの視聴者数を引いて、相対的にグラフを作っています。\n\n`constdataJson=topAnimes.map((anime)=>convertToMd(anime))constmainMd=json2md(dataJson)console.log(style+graphMd+mainMd)}MalRank();

Next.js+TypeScriptでマルチプロセス対応カスタムサーバ作成

$
0
0

Next.js+TypeScriptでマルチプロセス対応カスタムサーバ作成

カスタムサーバ

 Next.jsはWebServer機能を標準で内蔵していますが、マルチプロセスや特殊なセッション処理などを組み込む場合には、カスタムサーバという形でWebServer部分を自分で実装する必要があります。

 公式にサンプルはある物の、以外に日本語の情報が少ない、それどころかマルチプロセスやfastifyでの実装記事は皆無だったので、書いていきたいと思います。

マルチプロセス化について

 Next.jsを動かしているNode.jsは基本的にシングルスレッドで動作します。シングルスレッドといってもI/Oアクセスに関しては非同期で行われているため、無駄なブロックは起こらず、実用的な速度で動作することが可能です。

 ところが計算処理などをしている間は当然他の仕事は出来ません。マルチコアCPUなどでハードウエア的に余裕があっても、シングルスレッドである限りはせっかくのリソースが活用できないのです。

 これに対処するにはNext.jsをマルチスレッドではなく、マルチプロセス化するのが有効な手段となります。ありがたいことにNode.jsには、マルチプロセス化を簡単に実装するライブラリが標準提供されているので、カスタムサーバ化のコードを少し書くだけで、その恩恵を受けることが出来ます。

Fastifyに関して

 Node.jsでWebServer機能を実装するフレームワークとして有名なのはExpressです。しかし古い実装を引きずっているため、応答速度が遅いといわれています。今回はベンチマークで上位に位置するFastifyを使ってカスタムサーバを作ります。

インストールが必要な最低限のパッケージ

yarn add cross-env fastify next react react-dom
yarn add -D @types/node @types/react @types/react-dom ts-node-dev typescript

カスタムサーバの実装コード

 以下の二つのファイルを用意します。
 ちなみに環境変数でINDEXというのを子プロセスに渡していますが、workerにIDが振られるので実は無くてもかまいません

server/index.ts
importnextfrom"next";import*asosfrom"os";import*asclusterfrom"cluster";import{parse}from"url";importfastifyfrom"fastify";constdev=process.env.NODE_ENV!=="production";constclusterSize=Math.min(os.cpus().length,4);constportNumber=3000;if(cluster.isMaster){for(leti=0;i<clusterSize;i++)cluster.fork({INDEX:i});}else{constapp=next({dev});consthandle=app.getRequestHandler();constserver=fastify();app.prepare().then(()=>{server.all("*",(req,res)=>{returnhandle(req.raw,res.raw,parse(req.url,true));});server.listen(portNumber).then(()=>{console.log(`[${process.env.INDEX}]:http://localhost:${portNumber}`);});});}
server/tsconfig.json
{"compilerOptions":{"module":"commonjs","outDir":"../.next","esModuleInterop":true}}

 tsconfig.jsonを作成しているのは、Next.js管理下のpagesファイルなどとはTypeScriptのビルドの扱いが異なるからです。

スクリプト関係

 devはカスタムサーバ自体の自動リロードのため、ts-node-devを使っています。ただし.nextの中身はNext.js側が調整するので、無視指定が必要です。

package.json
{"name":"nextjs-custom","version":"1.0.0","main":"index.js","license":"MIT","scripts":{"dev":"ts-node-dev --ignore-watch \\.next -P server/tsconfig.json server/index.ts","build":"tsc -b server && next build","start":"cross-env NODE_ENV=production node .next/index.js","export":"next export"},"devDependencies":{"@types/node":"^14.14.22","@types/react":"^17.0.0","@types/react-dom":"^17.0.0","ts-node-dev":"^1.1.1","typescript":"^4.1.3"},"dependencies":{"cross-env":"^7.0.3","fastify":"^3.11.0","next":"^10.0.5","react":"^17.0.1","react-dom":"^17.0.1"}}

まとめ

 大したコード量も必要なくカスタムサーバが実装できました。マルチプロセスとFastifyのパワーによって、きっと快適SSRライフが送れることでしょう。

 ただしベンチマークを取った結果、それなりの負荷をかけても効果が顕著に出るのは2プロセスまでというオチでした。ベンチマークに関しては別記事を書く予定です。

Node.jsの基本 その2

$
0
0

Node.js備忘録として、書き始めました。その1はこちら
今回はほぼexpressの基本みたいな回です。

HTTP

HTTPはインターネットで支配的なプロトコルです。Node.jsはサーバー側クライアント側双方に適したモジュールを持っています。
Node.jsに興味を持っている人なら多くの場合express.jsというウェブアプリケーションフレームワークが存在するのを知っていると思います。expressはとても便利で僕も一度覚えてしまうともう他へ浮気できなくなってしまいました。koaやhapiなどのオルタナティブも存在しますが、Qiitaにおけるexpress.js記事の数は他の追随を許していませんから、初学者はexpress一択でしょう。
しかし、何にせよ基礎の基礎を知っておくのは大事なことです。ビルトインモジュールであるhttpを使ったサーバーとクライアントの書き方を見てみましょう。
その1でも用いたHello Worldの例は最も簡素なNode.jsを用いたHTTPサーバーです。

server.js
consthttp=require('http')http.createServer((req,res)=>{res.end("Hello World!")}).listen(3000,"127.0.0.1")

httpを用いたルーティング

server.js
consthttp=require('http')consturl=require('url').URLconstport=3000consthostName="127.0.0.1"http.createServer((req,res)=>{constpath=newurl(req.url,`http://${hostName}:${port}`)if(path.pathname==="/"){res.writeHead(200,{'Content-Type':'text/plain'})res.end('This is root page\n')}elseif(path.pathname==="/about"){res.writeHead(200,{'Content-Type':'text/plain'})res.end('this is about page\n')}else{res.writeHead(404,{'Content-Type':'text/plain'})res.end('404姉さん')}}).listen(port,hostName)console.log(`Server running at http://${hostName}`)

HTTPクライアント

client.js
consthttp=require('http')http.get('http://www.google.co.jp',(res)=>{if(res.statusCode==200){console.log('successful')}else{console.log('google is down again')}}).on('error',error=>{console.log(error)})

Express.js

Express.jsは最も採用率の高いウェブアプリケーションフレームワークです。これさえあればウェブアプリケーションを作成する際、まずほとんどの状況に対応できる機能が備わっています。
まずはインストールしてみましょう。

npm install -g express

これで環境のどこでもexpressのジェネレーター機能を使えるようになります。

express [web app name]

このコマンドで雛形を作成できます。初学ではどのようなモジュールを用意するべきかわかりやすいでしょう。

起動

cd [web app name]
npm install
node app.js

作成されたアプリのディレクトリを見ていきましょう。(express@4.17.1)

node_modules

インストールされているモジュールをここに保持しています。

package.json

ざっくり言えばこのウェブアプリケーションに関しての情報を保持しているファイルです。

binフォルダ

実行用のファイルを入れるためのフォルダです。

routes

ルーティング用のフォルダです。RESTfulなAPIを作成する上でルーティングは可視化面でもメンテナンス面でも重要で、ユーザーに関しての処理はすべて/userルートへ認証に関しての処理はすべて/authルートへなど個別のexpress.route関数ファイルを用意するのが良いでしょう。次の項目でより詳しく説明します。

public

スタイルシートやjavascriptファイル、画像などをこのフォルダに保存してアプリケーションにここを参照させるようにします。例としてはbootstrapやjQuerryなどでしょう。

views

ビューエンジンに指定された形式の同名ファイルを参照します。express generatorはjadeを使用しています。(個人的には少し可読性が低いにしろejsが好きです)
jadeというのはテンプレートエンジンで、いわば雛形です。動的なウェブページのレンダリングに向いています。

ウェブアプリケーションにおけるルーティング

ルーターの設定は少なくともNode.jsでウェブアプリケーションを作成するうえでとても重要です。どのHTTPリクエストにどのように処理するかの振り分けがルーティングです。

HTTPメソッド

Express.jsはルート定義の際にhttpメソッドとパスを組み合わせて使用します。
GET, POST, PUT, DELETE, などなど
手始めにはサーバーからデータを受け取るGETとサーバーへデータを送るPOSTを使用するところからでしょう。

GETとPOSTのリクエストのテストを例を書きますが、それに先立ってexpressのミドルウェアでHTTPリクエストの必要な情報を抽出してくれるbody-parserというモジュールをインストールしましょう。

npm install body-parser
server.js
constexpress=require('express')constbodyParser=require('body-parser')constserver=express()constport=3000server.use(bodyParser.urlencoded({extended:true}))server.use(bodyParser.json())//rootserver.get('/',(req,res)=>{res.send('hello world')})//about get routeserver.get('/about',(req,res)=>{res.send('about route get request called')})//about post routeserver.post('/about',(req,res)=>{console.log('called')constrequestBody=req.bodyconsole.log(requestBody)res.send(req.body)})server.listen(port,()=>{console.log('server listening on:'+port)})

ルートにおけるパラメータの扱い方

アプリケーションによってはRESTfulであるためにパスからパラメータを受け取る必要があります。expressにおいてはパスの最後に":[parameter_name]"で定義できます。

app.get('/api/users/:id',(req,res)=>{res.send('受け取ったIDは'+req.params.id+'です')})

クエリの扱い方

アプリケーションによってはクエリによる検索機能などを導入する必要があるでしょう。
Expressにおいてはパスに"?[query_parameter_name]=[query_value]"で定義されたHTTPコールを

req.query.query_parameter_name

でその値を扱うことができます。

server.get('/search/',(req,res)=>{res.send(req.query)})

Routeを外部モジュールとして扱う

小規模なアプリの構築には必ずしも要るとは限りませんが、可読性の向上のため、たとえばユーザーについての処理はすべて/userのルートへ、認証のための処理はすべて/authのルートへ、それぞれ個別のファイルとしてまとめて大元のapp.jsへエクスポートするのが推奨されるでしょう。

route/users.js
varexpress=require('express');varrouter=express.Router();router.get('/',function(req,res){res.send('/users called');});router.get('/all',(req,res)=>{res.send('/users/all called')})module.exports=router;

このルーターをserver.jsでインポートしましょう

server.js
constexpress=require('express')constbodyParser=require('body-parser')constusersRoute=require('./routes/users')constserver=express()constport=3000//users routeserver.use('/users',usersRoute)//rootserver.get('/',(req,res)=>{res.send('hello world')})server.listen(port,()=>{console.log('server listening on:'+port)})

ビューレンダリング

先程からずっと例に使っているres.send()という関数は受け取った値をテキスト及びHTMLとしてクライアントへ送ります。
その他に例えばres.json()という関数は受け取った値を有効なJSONファイルの形式で送信します。
res.render()という描画されてHTMLを返す関数に用いる、ビューテンプレートは非常に便利な機能です。たとえばブログを作成するとして、記事ごとにHTMLを書くのは冗長ですし非効率です。タイトルや本文のデータを受け取ってどのように当てはめて描画するかの雛形を作っておくことで、"/archives/:article_id" で対応するidの記事タイトルや本文を受け渡しするだけでよくて効率的です。

ビューテンプレートエンジンと呼ばれるものは色々あります。僕は個人的にejsをプロジェクトで使ってきたので覚えがありますが、ここではexpress generatorに用いられているjadeで例を出していきましょう。

まずjadeのモジュールをインストールしましょう

npm install jade

次にviewsフォルダを作成しindex.jadeファイルを作成しましょう

index.jade
doctype html
html
  head
    title Jade Example
  body
    h1 #{message}

jadeはインデントセンシティブなので開業とtabによる間隔で階層を形成する必要があり、閉じるためのタグがないHTMLを書いているような感じです。受け渡されたデータは#{}の中で変数名を指定します。

server.js
constexpress=require('express')constbodyParser=require('body-parser')constserver=express()constport=3000//set jade as its view engineserver.set('view engine','jade')//root path// render index.jade with message = 'hello world!' server.get('/',(req,res)=>{res.render('index',{message:"Hello World!"})})server.listen(port,()=>{console.log('server listening on:'+port)})

次へ

その3へ続きます。主にmongoDBに関してになると思います。

[エラー対処] Express チュートリアル populatedbのURLが機能しない

$
0
0

Express チュートリアルパート3: データベースの使用 (Mongooseを使用)

node populatedb <your mongodb url>

がつまづきポイントでして

your mongodb url に以下のyour_user_name_passwordの部分を変更してコマンドを実行すると思いますが、失敗し時間を取られたので、備忘録として残しておきたいと思います。

mongodb+srv://your_user_name:your_password@cluster0.a9azn.mongodb.net/local_library?retryWrites=true

Exampleとして

  • password → iekud

など

失敗する例として

  • dbUserPassword 推測ですが、デフォルトの文字列だと失敗する?
  • 12345 数字だけでは失敗するらしい?

失敗するがご存知の方がいらっしゃれば、コメントをしていただけると嬉しいです!

参考ページ
Express Tutorial Part 3: Using a Database (with Mongoose)
node populatedb url not working

Raspberry pi zero w に node.js をインストール

$
0
0

はじめに

Raspberry pi zero w に node.js をインストールしたくなった。Raspberry pi zero w をサーバーにし、React.jsでアプリケーションを作ってLAN内の別端末から利用したい、というのがその理由だ。気を遣う部分は、今回の対象が、「Raspberry pi zero である」という点である。RaspberryPi3や4とはCPUが違うのだ。Raspberry pi zero に使われているCPUはArmv6, RaspberryPi3や4はArmv7だそうで・・・。あと、Rasberry pi OS は 32bitOS ですよね。

準備

Raspberry pi zero w を Raspberry pi OS lite でセットアップし、まずはインターネットにつながっているLANまでの接続は行った。apt のアップデートができれば、準備オッケーというところではないだろうか。
(この下準備については、前回記事等が参考になるはず・・・。ちないに今回の案件は、前回の4GBディスクとは別のSDカードでRaspberry Pi zero をセットアップ。)

node.js インストール

いつものごとく、諸先輩方の知見を参照する。

  【Node.js】Raspberry Pi Zeroに最新のNode.jsをインストールする
  https://www.taneyats.com/entry/install-nodejs-on-raspberrypi-zero

上述先輩の記録資料を真似して、OS,CPUに合うソースファイルを公式から取得することにする。

ファイルの取得

見てみると、node-v12 以降は、Armv6のファイルが無いようだ・・・。そこで、次のとおりファイルを取得する。(2021年1月時点)

# まずは任意のディレクトリを作成(取得ソース展開用)
$ mkdir node_src

$ cd node_src
$ wget https://nodejs.org/dist/latest-v11.x/node-v11.15.0-linux-armv6l.tar.gz
$ tar -zxf node-v11.15.0-linux-armv6l.tar.gz

インストール

先達に倣って、/usr/local にディレクトリ丸ごとコピーして使えるようにする。

$ cd node-v11.15.0-linux-armv6l.tar.gz
$ rm CHANGELOG.md LICENSE README.md
$ sudo cp -R * /usr/local/

確認

コマンドのバージョンを確認してみる。

$ node -v
v11.15.0
$ npm -v
6.7.0

npm のバージョンを上げておく。

$ sudo npm install -g npm

$ npm -v
6.14.11

備考

yarn コマンドをインストールする。

$ sudo npm install -g yarn

おわりに

Raspberry pi zero w に node.js をセットアップした。作業時間は1時間くらい。通信量もさほどなく、コンパイル等も不要だったため、コマンドを打って待つ時間もさほどなかった。

Nodeで、SJISで、かつ、行ごとに列数が変動するcsvを読み込んでみた

$
0
0

Nodeにて、SJISで、かつ列数が行ごとに変動するcsvを読み込んで使う必要があったため、
試行錯誤して実装できたコードを残しておきます。

いろいろな参考コードがネットにありましたが、
「SJISで、非同期読み込みで実装」そのままのものは見つからず、
結構試行錯誤しました。
どなたかの参考になれば幸いにて。

constcsv=require('csv')constfs=require('fs')consticonv=require('iconv-lite')constcsvFilePath='./path/to/csv_file.csv'/**
 * csvデータの取得(SJISのファイル)
 */constgetCsv=asyncfunction(){returnnewPromise(resolve=>{fs.readFile(csvFilePath,function(err,data){//非同期処理なのでreadFileSyncではなくreadFileif(err)throwerrconstbuf=Buffer.from(data,'binary')//new BufferはdeprecatedなのでBuffer.fromを使うconstret=iconv.decode(buf,'Shift_JIS')//SJISをdecodeconstret2=iconv.encode(ret,'UTF-8')//UTF-8へencodecsv.parse(ret,{relax_column_count:true},//列数変動ファイルの読込オプションfunction(err,output){resolve(output)})})})}asyncfunctionmain(){constdata=awaitgetCsv()//非同期読込console.log(data)//UTF-8でのCSVデータ確認console.log(data.length)//行数の確認}main()//実行

参考:オプション設定で参考になったページ
csv parser本家ページ

ペアリングしてステレオ化したGoogle Home(Nest)にボイスコマンドでPodcastを流してもらう

$
0
0

2台をペアリングしてステレオスピーカー化したGoogle Nest mini(Google Home miniの新型)にボイスコマンドでPodcastをしゃべってもらおうと思ったら、意外とハマりどころが多く、結構な苦戦を強いられました。
苦労の末、どうにかやりたいことはできるようになったので、今回はその辺の話を書きたいと思います。
実現のためにいくつかのクラウドサービスを利用していますが、無料枠の範囲内で大丈夫です。

この辺の記事を参考にしています。

Google Homeで好きなポッドキャストをスマートに再生する
google-home-notifier で スピーカーグループを喋らせる

やりたいこと

google homeに「ok google、~~~を流して」とお願いすると、あらかじめ指定しておいたpodcastの最新話を再生してくれる。

必要なもの

  • IFTTTのアカウント
  • firebaseのプロジェクト(無料プランで大丈夫です)
  • 自宅のLAN内で常時インターネットに接続されていてnodejsが動作するPCなど
    • 僕はRaspberry pi を使っています

動作させる仕組み

  • Google home(Google Assistant)へのボイスコマンド入力を、IFTTTでフックする。
  • IFTTTのアクションでfirebaseのRealtime Databaseを更新する。
  • nodejsで組んだスクリプトでfirebaseの更新を監視しておき、更新を検知したらPodcastのRSSを取得&パースし、最新話のMP3のURLをGoogle Homeに渡す。
  • Google Homeが、受け取ったMP3を再生してくれる

実現手順

大まかな手順は「Google Homeで好きなポッドキャストをスマートに再生する」に書かれている通りですが、いくつか修正した方がよい箇所があります。

元記事からの変更点

環境によっては、いくつかの修正が必要です。

mdnsモジュールの一部機能がRaspberryPiで動かない問題の修正

依存ライブラリのgoogle-home-notifierの内部で利用されている「mdns」モジュールが、Raspberrypiでは正しく動作しないため、修正が必要です。

121行目を以下のように修正します

変更前

~/node_modules/mdns/lib/browser.js
,'DNSServiceGetAddrInfo'indns_sd?rst.DNSServiceGetAddrInfo():rst.getaddrinfo()

変更後

~/node_modules/mdns/lib/browser.js
,'DNSServiceGetAddrInfo'indns_sd?rst.DNSServiceGetAddrInfo():rst.getaddrinfo({families:[4]})

※ 引数に {families:[4]} を追加しています。

元記事に掲載されているRaspberrypiで動かすnodejsスクリプトの修正

podcastを再生するGoogle HomeをIP Addressで指定していますが、スピーカーのペアリングやグループ化をしているとうまく動きません。

IPアドレス指定ではなく、google-home-notifierは、対象のgoogle homeデバイスをIPアドレス指定ではなく内部名で指定することもできるようになっているため、内部名で指定するように変更します。

まず、再生したいGoogle Home(Nest)の内部名を調べます。

internal_name.js
varmdns=require('mdns');varbrowser=mdns.createBrowser(mdns.tcp('googlecast'));browser.start();browser.on('serviceUp',function(service){console.log('Device "%s" at %s:%d',service.name,service.addresses[0],service.port);});

実行結果は以下のようになります(デバイスの内部名の一部を伏字にしています)。

*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
*** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
Device "AQUOS-TV***************" at 192.168.0.111:8009
Device "Google-Home-Mini***************" at 192.168.0.22:8009
Device "Google-Nest-Mini***************" at 192.168.0.20:8009
Device "Google-Cast-Group***************" at 192.168.0.20:32000
Device "Google-Nest-Mini***************" at 192.168.0.21:8009

出力が終わったらCtrl+Cで終了

(グループ化・ペアリングしたスピーカーの場合には、Google-Cast-Group-**** のような名前になります)

上で取得した情報をもとに、引用元記事で書かれているraspberrypiで動かすスクリプトを以下のように修正&保存します(nodeのバージョンにもよるかもしれませんが、実行時エラーが出たのでそこも修正しています)。

raspberrypi.js
varFeedParser=require('feedparser');varfirebase=require('firebase');vargoogleHome=require('google-home-notifier');varrequest=require('request');constlang='ja';// const ip = '192.168.0.20'; //再生したいGoogle HomeのIPアドレス// googleHome.ip(ip, lang);constdeviceName='Google-Cast-Group*************';googleHome.device(deviceName,lang);constconfig={apiKey:'hoge',authDomain:'fuga.firebaseapp.com',databaseURL:'https://**************.firebaseio.com',projectId:'fuga',storageBucket:'',messagingSenderId:'piyo'};firebase.initializeApp(config);vardb=firebase.database();varref=db.ref('/');ref.on('child_changed',function(snapshot){// var url = ref.child('url').val();varurl=snapshot.val();if(url){playLatestPodcast(url);}ref.update({'podcast_url':''});// 変更をリセット});functionplayLatestPodcast(url){varreq=request(url);varparser=newFeedParser();varitems=[];req.on('response',function(res){this.pipe(parser);});parser.on('readable',function(){while(item=this.read()){items.push(item);}});parser.on('end',function(){googleHome.play(getLatestPodcastUrl(items),function(notifyRes){});});}functiongetLatestPodcastUrl(items){for(itemofitems){for(enclosureofitem.enclosures){varurl=enclosure['url'];if(url){returnurl;}}}return"";}

グループ化したスピーカーに対応させる。

再生するデバイスが1台のgoogle homeの場合には、これでうまくいきますが、Google homeがグループ化されていたり、ペアリングしているステレオ化されたスピーカーで再生しようとしている場合はうまく動きません。

google-home-notifier で スピーカーグループを喋らせる」の記事を参考に、~/node_modules/google-home-notifier/google-home-notifier.jsを修正します。

変更前

~/node_modules/google-home-notifier/google-home-notifier.js
deviceAddress=service.addresses[0];

変更後

~/node_modules/google-home-notifier/google-home-notifier.js
deviceAddress={};deviceAddress.host=service.addresses[0];deviceAddress.port=service.port;

※ 修正が必要な個所は2か所あります。

これで、作業はすべて完了です。

IFTTTで設定した「ok google、~~~~を流して」というフレーズを言えば、目的のpodcastの最新話が自動で再生されるかと思います。

同じ仕組みを利用して、miniDLNAなどで組んだDLNAサーバのプレイリストURLをgoogle homeに渡してあげれば、「ok google、NASの音楽を流して」という音声コマンドでNASに保存した音楽をシャッフル再生してくれるような仕組みなども作れそうです。

機会があればやってみたいと思います。


M1マシンでflowが起動しない問題の対応

$
0
0

事象

M1(apple silicon)マシンでrosettaを使って入れたnode.jsで npx flowなどでflowを起動すると以下のようなエラーが出て起動に失敗する

Launching Flow server for /Users/sogasawara/progate
Spawned flow server (pid=6001)
Logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.log
Monitor logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.monitor_log
Launching Flow server for /Users/sogasawara/progate
Spawned flow server (pid=6008)
Logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.log
Monitor logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.monitor_log
Launching Flow server for /Users/sogasawara/progate
Spawned flow server (pid=6012)
Logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.log
Monitor logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.monitor_log
Lost connection to the flow server (0 retries remaining): -Out of retries, exiting!

ログ

[2021-01-27 17:22:16.301] argv=/Users/sogasawara/progate/node_modules/flow-bi
Unhandled exception: Unix.Unix_error(Unix.EINVAL, "ftruncate", "")
Raised by primitive operation at file "hack/heap/sharedMem.ml", line 82, char
Called from file "hack/heap/sharedMem.ml", line 146, characters 5-26

環境

Mac Mini(M1), node v10.16.3 (rosetta), flow 0.90.0

解決策(workaround)

.flowconfigに以下のオプションを追加する

[options]
sharedmemory.heap_size=2147483648

なお、公式のissueに問題の対応方法が書いてあります
https://github.com/facebook/flow/issues/8538

heap_sizeはマシンのメモリによって変わるとか。

私のマシンはMac miniのメモリ16GBですが上記の値で動きましたがマシンによって調整が必要そうです

pug内のパスをnodeの開発環境で切り替える

$
0
0

テスト環境/本番環境でディレクトリ階層が違というプロジェクトがあり、pugの変数をビルドのたびに手動で切り替えるようにしていました。
「nodeでビルドするんだからnodeの環境開発で切り替えれるっしょ!!」
と思ってやってみたらやっぱり出来たので、備忘録として書き残しておきます。

環境

  • pug ^3.0.0
  • html-webpack-plugin ^4.5.1
  • pug-loader ^2.4.0

pugは、pug-loaderhtml-webpack-pluginを用いてwebpack内でビルドしています。
ここでのミッションは、webpack経由でどうやってpugに環境変数を埋め込むか?です。
(下記の方法はpug出なくても素のhtmlでも可能です。)

pug内に環境変数を埋め込む

plugins:[newHtmlWebpackPlugin({title:'My App',filename:'assets/admin.html'})]

これを

plugins:[newHtmlWebpackPlugin({title:'My App',filename:'assets/admin.html',environment:process.env.NODE_ENV})]

こうですね。environmentはドキュメントなどにある提供されているプロパティではなく勝手につけたプロパティのようです。
つまり fooでも testでもなんでもOKです。

- var appEnv = htmlWebpackPlugin.options.environment;

pug内で上記のよう先ほど追加したプロパティ経由で process.env.NODE_ENVにアクセスすれば、dev/proでpugを分岐出来ます。

もちろん、コマンドを叩くときはNODE_ENVのセットを忘れずに。

package.json
"dev":"NODE_ENV=development webpack --mode production","build":"NODE_ENV=production webpack --mode production"

おまけ HTMLでは?

<bodyclass="<%= htmlWebpackPlugin.options.environment %>">

参考サイト

https://stackoverflow.com/questions/39902197/how-can-i-pass-webpack-environment-variables-in-html

【docker】Node.jsのコンテナ立ち上げてサンプルアプリを動かしたい

$
0
0

この記事の目標

dockerのコンテナ立ち上げて、このアプリ↓をローカル環境で立ち上げる

https://github.com/justadudewhohacks/face-api.js

GitHubからリポジトリcloneする

ローカルの任意のディレクトリにcloneする

$ git clone https://github.com/justadudewhohacks/face-api.js

docker hubからnode.jsのイメージpullしてくる

alpineが軽量でオススメらしいので、v14のをpullする

$ docker pull node:14-alpine

イメージをpullできたことの確認

$ docker images

コンテナ立ち上げる

docker run -it-v$PWD:/workspace -p 8100:3000 --name node-face-api node:14-alpine /bin/ash
  • -itで、コンテナ内で入力可能な状態にする
  • -v $PWD:/workspaceで、カレントディレクトリのファイルをコンテナ内の「workspace」ディレクトリにマウントするように指定
    • そのため、リポジトリをcloneしたディレクトリで作業する必要あり
    • $PWD$(pwd)といった指定方法もありらしい
  • -p 8100:3000で、ホスト側のポート8100とコンテナ側のポート3000を繋ぐ
    • このホスト側のポート番号は空いているものを適宜使う
    • コンテナ側はよしなに設定
    • ポート使用状況はMacの場合「ネットワークユーティリティ」で確認できる
  • --name node-face-apiで、コンテナ名を「node-face-api」に設定
  • node:14-alpineで、イメージとバージョン(TAG?)の指定
  • /bin/ashで、コンテナ内で使うコマンドを指定

今回は使ってないが、以下のオプションもよく使われる

  • --rmで、コンテナから出た時にコンテナ削除

※ちなみに、コマンドの記述順は、以下の通りらしい

$ docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

起動時にエラーが起こって上手くコンテナに入れなかった場合、以下の手順でコンテナ削除し、
runコマンド見直してから再度試す

# コンテナ一覧表示$ docker ps -a# 起動(作成)に失敗したコンテナのIDを指定してコンテナ削除$ docker rm[CONTAINER ID]

コンテナ内でアプリケーションを立ち上げる

コンテナ作成して中に入ると、ホストのファイル群がworkspaceディレクトリ内にあるはずなので、確認する

$ cd workspace
$ ls

あとは、face-ap.jsのチュートリアル通りに進めていくだけ

$ cd examples/examples-browser
$ npm install$ npm start

ホストマシンのポートは8100に指定したので、以下にアクセス

http://localhost:8100/

それっぽいページが表示されればOK!

参考

anyenvとnodenvを使ったNode.jsの環境構築

$
0
0

本記事では、anyenvnodenvを使ってMacにNode環境を構築する。

nodenvとは

プロジェクト(ディレクトリ)ごとに、Node.jsのバージョンを管理することができるバージョン管理ツールのこと。

なぜnodenvを使用するのか?

仮に、nodenvを使用せずに、あるプロジェクトAのためにNodeのv7系を固定でインストールすると、別のプロジェクトBにおいてv8系を使用する必要があった際にはNodeのアップデートが必要となってしまう。

nodenvを使用したバージョン管理をしていると、上記のようなプロジェクトごとに異なるバージョンを使用する際の手間をなくすことができる。


今回はnoodenvを包括するanyenvを使ってNodeのバージョン管理を行える環境を構築していく。nodenvの他にもRubyやPythonのバージョン管理を行う〇〇envも存在し、anyenvはこれらのバージョン管理ツールを包括的に管理するツールである。

anyenvのインストール

以下のコマンドを実行しanyenvをインストールする

% brew install anyenv

nodenvをインストールする前にanyenvの初期化が必要なため初期化を行う。

% anyenv install--init

... Do you want to checkout ? [y/N]: y <- yと答える

remote: Total 62 (delta 1), reused 1 (delta 0), pack-reused 57
Unpacking objects: 100% (62/62), done.

Completed!

anyenvのインストールに成功したので、次にnodenvのインストールを行う。

nodenvのインストール

以下のコマンドを実行しnodenvをインストールする

% anyenv install nodenv

...
Resolving deltas: 100% (76/76), done.
~

Install nodenv succeeded!
Please reload your profile (exec$SHELL-l) or open a new session.

プロファイルの更新のため、以下のコマンドも続けて実行する。

% exec$SHELL-l

インストール可能なバージョンを以下コマンドの実行結果から確認することができればインストールに成功。

% nodenv install-l
0.1.14
0.1.15
...
...

nodenvコマンドの実行に失敗した時の対処方

.zshrc ファイルが未作成のため、nodenvのパスが設定されていなく失敗する場合は、以下の対処を行うことを勧める

  • vi ~/.zshrcを実行
  • eval "$(anyenv init -)"を書き込み保存する
  • 再度、anyenv install nodenvを実行する

Node.jsのインストール

以下のコマンドを実行しNode.jsをインストールする。今回は12.20.0をインストールする。

% nodenv install 12.20.0

インストール済みのバージョンは以下のコマンドから確認できる。12.20.0が表示されれば無事にNode.jsのインストールに成功している。

% nodenv whence npm
12.20.0

グローバルのNode.jsのバージョンを指定

以下のコマンドで、インストール済の12.20.0をグローバルなバージョンとして指定する。

% nodenv global 12.20.0

動作確認

nodeコマンドを実行し、12.20.0が表示されれば無事にNode環境の構築ができている。

% node -v
v12.20.0

ディレクト毎にバージョンを切り替えたい場合

今回は12.20.0のNodeバージョンを使用できるようにしたが、特定のディレクトリでは8.0.0を使用したくなったときの手順を最後に紹介する。

  • Node.js8.0.0をインストール
% nodenv install 8.0.0
  • 8.0.0を使用したディレクトに移動する。(今回はnode-8)
% cd node-8
  • localオプションで8.0.0を指定
% nodenv local 8.0.0
  • 対象ディレクトリ配下でnode -vを実行し8.0.0が表示されれば成功
% node -v
8.0.0

nodenv local 8.0.0実行時には、指定したバージョンが記載された.node-versionファイルが生成される。このファイルに指定された値を変更することで使用するバージョンも変更することができる。

参考記事

https://qiita.com/kyosuke5_20/items/eece817eb283fc9d214f

https://www.to-r.net/media/anyenv/

node + expressでJWT (2021年1月)

$
0
0

随分昔に同じ趣旨の記事を書いたのですが、再度書き直してみます(あまり変わってない)。

準備

作業場作成

ひとまず作業場所を確保し、必要なモジュールをインストール。body-parserはもういらない。

mkdir jwt-test
cd jwt-test
npm init -y
npm install express jsonwebtoken

実装前に検証

実装に入る前にJWTの生成と検証のコア部分をワンライナーで検証。

生成

なにやらややこしそうに思うが、[ヘッダ].[ペイロード].[署名]をそれぞれbase64でエンコードしているだけ。
署名部はsecret(ここではmy_secretという文字列)を利用してHS256で署名している。

node -e"console.log(require('jsonwebtoken').sign({username:'hoge'},'my_secret'))"

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTA3Nzl9.MbcALpRUEu9KxGZ5S1qLoieb41_dr-i2o__QVnlVTow

サイン時に、 sign({username:'hoge'},'my_secret',, { expiresIn: '1h' })とすることで有効期限を設定可能。

検証

node -e"console.log(require('jsonwebtoken').verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTA3Nzl9.MbcALpRUEu9KxGZ5S1qLoieb41_dr-i2o__QVnlVTow','my_secret'))"{ username: 'hoge', iat: 1611790779 }

メモ

大規模?な利用シーンでは署名と検証に公開鍵技術を利用する場合もあると思うが、jsonwebtokenは対応しているよう。詳しくは本家サイトを見る。

実装

では、簡単なJWTを利用したAPI認証機能を実装する。主な機能は以下の感じ。

  • /loginにusername, passwordをPOSTで送信し、認証OKならtoken(JWT)を発行。
  • /protectedへのアクセスにはAuthorizationヘッダにBearer+tokenを付与し認証OKならコンテンツを戻す。
  • 認証機構はverifyToken()という外部関数とし実装し、app.get('/protecte')のミドルウエアとして適用する。

では実装します。

index.js
varexpress=require('express');varapp=express();varjwt=require('jsonwebtoken');app.use(express.json());app.use(express.urlencoded({extended:true}));app.listen(3000,function(){console.log("App start on port 3000");})//認証無しAPIapp.get('/',function(req,res){res.json({status:"OK"});})//認証+Tokenの発行app.post('/login',function(req,res){//ID,PW取得varusername=req.body.username;varpassword=req.body.password;//認証//実際はDB等と連携if(username==="hoge"&&password==="password"){//token生成(フォマットは適当だが、有効期限を設定)consttoken=jwt.sign({username:username},'my_secret',{expiresIn:'1h'});res.json({token:token});}else{res.json({error:"auth error"});}})//認証有りAPIapp.get('/protected',verifyToken,function(req,res){res.send("Protected Contents");})functionverifyToken(req,res,next){constauthHeader=req.headers["authorization"];//HeaderにAuthorizationが定義されているかif(authHeader!==undefined){//Bearerが正しく定義されているかif(authHeader.split("")[0]==="Bearer"){try{consttoken=jwt.verify(authHeader.split("")[1],'my_secret');//tokenの内容に問題はないか?//ここでは、usernameのマッチと有効期限をチェックしているが必要に応じて発行元、その他の確認を追加//有効期限はverify()がやってくれるみたいだがいちおう・・・if(token.username==="hoge"&&Date.now()<token.exp*1000){console.log(token);//問題がないので次へnext();}else{res.json({error:"auth error"})}}catch(e){//tokenエラーconsole.log(e.message);res.json({error:e.message})}}else{res.json({error:"header format error"});}}else{res.json({error:"header error"});}}

実装できたら実行。

node index.js

動作確認

curlを利用して動作確認をしてみます。

tokenの取得

まずは認証してtokenを取得します。

curl -s-X POST -H'Content-Type: application/json'-d'{"username":"hoge","password":"password"}' http://localhost:3000/login

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTEyMDksImV4cCI6MTYxMTc5NDgwOX0.Gy_3r3T-AQG8iL28LE1xzVNMEJDKTtgTyMRiaSNpQiM"}

curl -X POST -d "username=hoge&password=password" http://localhost:3000/loginでも可。

tokenの利用

取得したtokenを利用して/protectedにアクセスしてみます。

curl -X GET http://localhost:3000/protected -H"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTEyMDksImV4cCI6MTYxMTc5NDgwOX0.Gy_3r3T-AQG8iL28LE1xzVNMEJDKTtgTyMRiaSNpQiM"

Protected Contents

うまく表示されました。

その他

リフレッシュはどうする?

調べ中。とりあえず下記を見る。まあ、実運用ではexpiredであれば再ログインさせ、そのときに再取得しればいいとは思う。
https://solidgeargroup.com/refresh-token-autenticacion-jwt-implementacion-nodejs/

Viewing all 8823 articles
Browse latest View live