20210128のNode.jsに関する記事は11件です。

React入門!!の前に知っておきたいNode.jsとJavaScriptの知識

対象読者とこの記事から何が得られるのか

そろそろReactの勉強をはじめてみたい、勉強したいと思っているけど、以下のようなことを考えて前に進めない方向けの記事です。

  • JavaScriptの知識レベルはこれくらいで足るのか?
  • 他に知っておくことはないのかな?

この記事を読むことでReact入門の前に知っておきたいNode.jsとJavaScriptの知識を得ることができると思います。

Node.jsをざっと知る

Node.jsを簡単に言うと

JavaScriptをブラウザではなくPythonやRubyなどと同じようにターミナル上で動かすことができるようにするための実行環境のことであり、バックエンド言語としてJavaScriptを使うことができ、ファイル操作などのOSの機能にアクセスできます。

公式ページだと以下のように記載されています。

Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。

Node.jsが注目を浴びたのはC10K問題の解決策である(イベント駆動により大量のリクエストを同時に処理できるスケーラビリティを備えている・ノンブロッキングI/Oモデルを採用しており、I/Oの処理を待たずに次の処理を始めることができるので、大量のデータ処理が可能)というのが理由です。またバックエンドも同じ言語(JavaScript)で書けたら効率的ではないかというのも理由の一つです。フロントエンド開発はJavaScriptほぼ一択ということもあり、こういう流れになったのではないかと思います。

なぜReactアプリ開発でNode.jsが必要なのか

ではなぜバックエンド開発のために開発されたNode.jsがReactアプリ(フロントエンド)開発で必要になってきたのでしょうか。

Reactのような大規模なアプリケーションを開発するとなると、様々なパッケージが必要になり、そのパッケージたちが特定のバージョンで依存しあっています。Node.jsはそのパッケージのインストールと整合性の管理を解決するために必要であり、npm(Node Package Manager)がその解決を担っています。

もともとはバックエンド開発のためにnpmは使われるパッケージ管理システムであったが、フロントエンド用のパッケージを提供するのにも使われるようになり、最近では使用用途としてはフロントエンドのほうが多くなってきているそうです。

その他以下のような用途でも使われます。

  • JavaScriptやCSSファイルをバンドルできる
  • ブラウザ実行時にpolyfill※するのではなく 最初からコンパイルしておける
  • ローカル環境で開発用のHTTPサーバを起動してアプリケーションを稼働させることができる
  • ユニットテストやE2Eテストなどのテストを実行できる
  • ローカル環境で構文解析等実行できる

※polyfillとは最近の機能をサポートしていない古いブラウザーで、その機能を使えるようにするためのコードです。

Node.jsをインストール

公式サイトからインストールしたり、MacならHomebrew・Windowsならwingetなどでインストールできますが、プロジェクトごとに異なるバージョンの環境を共存させる必要があるので、バージョンマネージャー(nvmやnodenv)を使ってインストールしましょう。

Node.jsでプロジェクト作成

作成したいディレクトリに移動してターミナルで以下コマンドでプロジェクトを作成できます。

対話形式でプロジェクト名など聞かれますので、全て入力するとpackage.jsonが作成されます。

$ npm init

-yをつければ、全てデフォルトのままプロジェクト作成できます。

$ npm init -y

npm-scripts

package.jsonのscriptsに記載されているコマンドを以下で実行できます。

$ npm run xxx

では試しにnpm-scriptsでプログラムを実行してみましょう。
まずindex.jsを作成する。

index.js
console.log("Hello World!!");

package.jsonにstartでのコマンドを追加する。
node.jsではnode+ファイル名 でそのファイルのプログラムを実行できます。

"scripts": {
  "start": "node index.js"
}

以下コマンドで実行する。

$ npm run start

以下出力されれば成功です。

Hello World!!

start,stop,restart,testの場合はrunを省略して実行できます。

$ npm start

Reactアプリ開発ではcreate-react-appというコマンドで新規プロジェクトが作成できますが、こちらで作られたプロジェクトだと以下が記載されています。たとえばnpm run startを実行するとreact-scripts startが実行され、ローカル環境でReactアプリが起動できます。react-scriptsはcreate-react-appで作られたプロジェクトの中でBabelやwebpackなどが裏で動かすことができます。

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},

Yarnとは

npmコマンドのFacebookによる改良版で記述が短い・実行が速いなどのメリットがある。
Yarnがグローバルインストールされていた場合はCreate React App使用時にyarnがデフォルトになるので、npmコマンドを使いたいなら--use-npmオプションを指定しましょう。

Yarnのコマンド

  • yarn (install) はプロジェクトのルートディレクトリに存在するpackage.jsonの内容を参照して、依存関係のあるパッケージをすべてインストールする
  • yarn add xxx で指定したパッケージをインストールする
  • yarn remove xxx で指定したパッケージをアンインストールする
  • yarn upgrade xxx で指定したパッケージを最新バージョンに更新する
  • yarn info xxx で指定したパッケージの情報を表示する

上記のnpm run xxxは以下コマンドで同じことができます。

yarn xxx

JavaScriptをざっと知る

変数宣言について

  • varは再宣言も再代入も可。スコープはブロックをすり抜ける。
  • letは再代入のみ可。スコープはブロック内。
  • constはどちらも不可。スコープはブロック内。

constを第一選択肢として、どうしても仕方ない場合のみletを使う。
varは使用しないようにしましょう。

var name1 = "瀬戸熊";
name1 = "佐々木"      // 再代入可
var name1 = "滝沢"    // 再宣言可

let name2 = "瀬戸熊";
name2 = "佐々木"      // 再代入可
let name2 = "滝沢"    // 再宣言不可:'name2' has already been declared

const name3 = "瀬戸熊";
name3 = "佐々木"      // 再代入不可:"name3" is read-only
const name3 = "滝沢"  // 再宣言不可:'name3' has already been declared

スコープに関してもvarを使うと以下のようになり、安全ではないのでletやconstを使うべきである。

var name1 = "高宮";
if (true) {
  var name1 = "二階堂";
  var name2 = "魚谷";
  console.log(name1); // 二階堂
  console.log(name2); // 魚谷
}
console.log(name1); // 二階堂(ブロック内でも書き換え可能なので危険)
console.log(name2); // 魚谷(ブロック内で定義したものがブロック外でも参照できる)

letだとブロック内で書き換えや定義しても、ブロック外に影響を与えない。

let name1 = "高宮";
if (true) {
  let name1 = "二階堂";
  let name2 = "魚谷";
  console.log(name1); // 二階堂
  console.log(name2); // 魚谷
}
console.log(name1); // 高宮
console.log(name2); // name2 is not defined

データ型について

プリミティブ型

以下7種類ある。

  • Boolean型
  • Number型
  • BigInt型
  • String型
  • Symbol型
  • Null型
  • Undefined型

falsyな値

MDNによると、falsyとは偽値 (falsy または falsey な値) は、 Boolean コンテキストに現れたときに偽とみなされる値です。

以下8種類あります。

  • false
  • 0
  • -0
  • 0n
  • Nan
  • ""(空文字)
  • null
  • undefined

以下はifブロックを実行しません。

if (false){}
if (null){}
if (undefined){}
if (0){}
if (-0){}
if (0n){}
if (NaN){}
if (""){}

関数

関数宣言文と関数式

定義の方法は関数宣言文による定義と関数式による定義があるがconstを使った関数式による定義が推奨されています。
無名関数は定義時に名前を与えられない関数のことで、関数式は変数に無名関数を入れているようなもの。
変数に関数式を代入することになるので、関数宣言文と違って先に定義しておかないと使えません。

// 関数宣言文による定義
function Introduce1(name) {
  return `私の名前は${name}です。`
} 
// 関数式による定義 
const Introduce2 = function (name) {
  return `私の名前は${name}です。`
};

アロー関数

アロー関数は引数が一つだと括弧の省略ができる。(推奨はされていないらしいが・・・)
また、retern文が1行だとreturnも省略できます。

// アロー関数式
const Introduce3 = (name) => {
  return `私の名前は${name}です。`
};
// アロー関数式、さらに省略記法
const Introduce4 = name => `私の名前は${name}です。`;

デフォルト引数

デフォルト値が設定された引数は省略が可能。
以下の例では第二引数を省略するとageが18で代用される。

const Introduce5 = (name, age = 18) => `私の名前は${name}です。${age}歳です。`;
console.log(Introduce5("白鳥", 32)); // 私の名前は白鳥です。32歳です。
console.log(Introduce5("松本")); // 私の名前は松本です。18歳です。

Rest Parameters

最後の引数に...を付けることで残りの引数を配列として受け取れる。

const Names = (name1, name2, ...rest) => {
  console.log(name1);   // 瀬戸熊
  console.log(name2);   // 佐々木
  console.log(rest);    // ["滝沢","二階堂","高宮"]
};
Names('瀬戸熊', '佐々木', '滝沢', '二階堂', '高宮');

Reactで開発する上で良く使う構文

プロパティ名のショートハンド

プロパティのキー名と値を同じにする。

const name = "瀬戸熊";
const obj = { name };  // = const obj = { name: name };
console.log(name); // { name: "瀬戸熊" }

分割代入

配列とオブジェクトの値を分割して代入する。

const data = [180, 70]
const [height, weight] = data;
console.log(`私の身長は${height}cmで、体重は${weight}kgです。 `); // 私の身長は180cmで、体重は70kgです。

const user = { name: "瀬戸熊", age: 50 };
const { name, age } = user;
console.log(`私の名前は${name}です。${age}歳です。`); // 私の名前は瀬戸熊です。50歳です。

スプレッド構文

const names1 = ["瀬戸熊", "佐々木", "滝沢"];
const names2 = [...names1, "二階堂", "高宮"];
console.log(names2); // [ "瀬戸熊","佐々木","滝沢","二階堂","高宮" ]

const users1 = { name: "瀬戸熊", age: 48, sex: "" };
const users2 = { ...users1, group: "renmei", grade: "A1" };
console.log(users2); // { name: "瀬戸熊", age: 50, sex: "男", group: "renmei", grade: "A1" }

オブジェクトのコピー

分割代入を用いてオブジェクトをコピーできます。

しかし、こちらはシャローコピーと言ってオブジェクトの深さが1段階までしか有効ではない。

オブジェクトの深さが2段階目の値を変更すると、コピー元の値まで変更されてしまう。

const user1 = { name: "瀬戸熊", age: 50, sex: "" };
const user2 = { ...user1 };
console.log(user2); // { name: "瀬戸熊", age: 50, sex: "男" }
console.log(user2 === users1); // false

const user3 = { name: "瀬戸熊", age: 50, sex: "", group: { group: "連盟", grade: "A1" } };
const user4 = { ...user3 };
console.log(user4);  // user4:  { name: '瀬戸熊', age: 50, sex: '男', group: { group: '連盟', grade: 'A1' } }
user4.group.group = "協会";
console.log(user3);  // user3:  { name: '瀬戸熊', age: 50, sex: '男', group: { group: '協会', grade: 'A1' } }
console.log(user4);  // user4:  { name: '瀬戸熊', age: 50, sex: '男', group: { group: '協会', grade: 'A1' } }

完全なコピー(ディープコピー)の方法はいくつかあるが、JSONパースを用いる方法は以下の通り。

プロパティにDateオブジェクトや関数が入ってた場合はうまく動かないので注意。

const user3 = { name: "瀬戸熊", age: 50, sex: "", group: { group: "連盟", grade: "A1" } };
const user4 = JSON.parse(JSON.stringify(user3));
console.log(user4);  // user4:  { name: '瀬戸熊', age: 50, sex: '男', group: { group: '連盟', grade: 'A1' } }
user4.group.group = "協会";
console.log(user3);  // user3:  { name: '瀬戸熊', age: 50, sex: '男', group: { group: '連盟', grade: 'A1' } }
console.log(user4);  // user4:  { name: '瀬戸熊', age: 50, sex: '男', group: { group: '協会', grade: 'A1' } }

他にはLodashのcloneDeep()を使う方法などある。

ショートサーキット評価(短絡評価)

  • || は左辺がfalsyな値だと評価が右辺に渡される。
  • ?? は左辺がnullかundefinedだと評価が右辺に渡される。
  • && は左辺がtruthyな値だと評価が右辺に渡される

|| はちょっと前に賛否ありましたが、一応よくでてくるので記載しておきます・・・。

const name1 = '瀬戸熊';
const name2 = '佐々木';

true && console.log(name1); // 瀬戸熊
false && console.log(name1); // 出力なし
true || console.log(name2); // 出力なし
false || console.log(name2); // 佐々木
null ?? console.log(name2); // 佐々木
undifined ?? console.log(name2); // 佐々木

配列・オブジェクトの処理

map,filter,find,findIndex,every,some

const dataset = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(dataset.map((data) => data * 3)); // [ 3, 6, 9, 12, 15, 18, 21, 24, 27 ]
console.log(dataset.filter((data) => data > 5)); // [ 6, 7, 8, 9 ]
console.log(dataset.find((data) => data > 5)); // 6
console.log(dataset.findIndex((data) => data > 5)); // 5
console.log(dataset.every((data) => data > 5)); // false
console.log(dataset.some((data) => data > 5)); // true
  • map():与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成する。
  • filter():与えられた関数によって実装されたテストに合格したすべての配列からなる新しい配列を生成する。
  • find():提供されたテスト関数を満たす配列内の 最初の要素の値を返します。見つからなかった場合はundefinedを返す。
  • findIndex():配列内の指定されたテスト関数を満たす最初の要素の位置を返します。テスト関数を満たす要素がない場合を含め、それ以外の場合は-1 を返します。
  • every():配列内のすべての要素が指定された関数で実装されたテストに合格するかどうかを真偽値で返します。
  • some():配列の少なくとも一つの要素が、指定された関数で実装されたテストに合格するかどうかを真偽値で返します。

reduce,sort

  • reduce():配列の各要素に対して (引数で与えられた) reducer関数を実行して、単一の出力値を生成します。
  • sort(): 配列の要素をソートします。既定のソート順は昇順で、要素を文字列に変換してから、UTF-16コード単位の値の並びとして比較します。
const dataset = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(dataset.reduce((a, b) => a + b)); // 45
console.log(dataset.sort((a, b) => a > b ? -1 : 1)); // [ 9, 8, 7, 6, 5, 4, 3, 2, 1 ]

forEach,for...of

  • forEach():メソッドは与えられた関数を、配列の各要素に対して一度ずつ実行します。
  • for...of:反復可能オブジェクトなどに対して、反復的な処理をするループを作成します。

本来なら使わないほうがいいが、どうしても使う場合はforEach推奨。

const dataset = [1, 2, 3, 4, 5, 6, 7, 8, 9];
dataset.forEach((data) => {
  if (data % 2 === 0) {
    console.log(`${data}は偶数です。`);
  }
});
for (let data of dataset) {
  if (data % 2 === 0) {
    console.log(`${data}は偶数です`);
  }
}

includes

特定の要素が配列に含まれているかどうかを true または false で返します。

const dataset = [1, 2, 3, 4, 5];
console.log(dataset.includes(1)); // true
console.log(dataset.includes(7)); // false

また、以下のような||演算子が繰り返し使われるコードを完結に書けます。

if ( x === 'a' || x === 'b' || x === 'c' ) {
  console.log('ok')
}
if (['a', 'b', 'c'].includes(x)) {
  console.log('ok')
}

Object.keys,values,entries

  • Object.keys():プロパティのキーのリストを配列で取得できる
  • Object.values():プロパティ値のリストを配列で取得できる
  • Object.entries():プロパティのキーと値が対になった2次元配列を取得できる
const user = {
  id: 1,
  name: '瀬戸熊',
  age: 50,
};
console.log(Object.keys(user));
// [ 'id', 'name', 'age']

console.log(Object.values(user));
// [ 1, '瀬戸熊', 50 ]

console.log(Object.entries(user));
// [
//   [ 'id', 1 ],
//   [ 'name', '瀬戸熊' ],
//   [ 'age', 50 ],
// ]

非同期処理

Promise

PromiseはES2015から導入されたJavaScriptの組み込みオブジェクトで、非同期処理の最終的な完了処理 (もしくは失敗) およびその結果の値を表現するものであり、Promiseを使うことによって、非同期処理の完了を待って次の処理を行うというのがJavaScriptでも簡単(それ以前はコールバック関数で書くしかなかったので、連続して非同期処理を行う場合にコールバック地獄となる)にできるようになります。

以下例文

  • 最初のresolve()に渡したものが.then()の引数のvalueになり、その.then()内でreturnしたものが次の.thenのvalueになる。
  • reject()に渡したものが、errorとしてcatch()で受け取れる。
  • finally()に渡された関数は必ず最後に実行される。(ES2018から)
const promise = new Promise((resolve, reject) => {
  if (isSuccess) {
    resolve('成功1');
  } else {
    reject(new Error('失敗'));
  }
});
promise.then((value) => {
    console.log(value);
    return '成功2';
  })
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.error(error);
  })
  .finally(() => {
    console.log('完了');
  });

axiosを使ってAPIをたたいてみる
async/awaitを使わない場合

const getUser = (userId) => {
  return new Promise(function (resolve, reject) {
    axios
      .get(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then((response) => resolve(response.data))
      .catch((error) => reject(error.response.status));
  });
};

getUser(1)
  .then((user) => {
    console.log(user);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
  });

async/awaitを使った場合

関数宣言時にasyncキーワードを付与するとその関数は、返される値がPromise.resolve()によってラップされたものになる。

asyncをつけた非同期関数内では他の非同期関数をawaitをつけて呼び出すことができる。

const getUser2 = async (userId) => {
  try {
    const response = await axios.get(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    return response.data;
  } catch (error) {
    throw error.response.status;
  }
};
const main = async () => {
  try {
    const user = await getUser2(1);
    console.log(user);
  } catch (error) {
    console.error(error);
  } finally {
  }
};
main();

モジュール

名前付きエクスポート

1ファイルでいくらでもエクスポートできる。

src/export.js
const NAME = "瀬戸熊";
const AGE = 50;
const Introduce = (name) => `私の名前は${name}です`
export { NAME, AGE, Introduce };

or

export const NAME = "瀬戸熊";
export const AGE = 50;
export const Introduce = (name) => `私の名前は${name}です`

名前付きエクスポートの場合は{}をつけてインポートする

src/inport.js
import { NAME, AGE, Introduce } from "./export"

デフォルトエクスポート

1ファイル1回までしかエクスポートできない

src/export.js
export const NAME = "瀬戸熊";
export const AGE = 50;
const Introduce = (name) => `私の名前は${name}です`
export default Introduce;

デフォルトエクスポートの場合は{}はつけないでインポートする。名前も自由に命名できる。

src/inport.js
import Introduce, { NAME, AGE } from "./export"

まとめ

React入門の前に知っておきたいNode.jsとJavaScriptの知識を簡単にまとめました。

今回記載した項目が大体頭に入っていれば、Reactに入門してもよいのではないかと思います。

このほかにもクラスやthisの扱いなど学ぶべきものはいっぱいありますが、Reactが関数コンポーネント主体になってきたのであまり使わない印象があるので、一旦は飛ばしてもよいかと思い省略しました。(理解はしておいたほうがいいですが)

あとTypeScriptはReactでの開発においてマストなものになりつつあるので、早めにTypeScriptでReactを書けるように学んでいったほうがいいと思います。

間違っていたり、これも理解しておくべきなどありましたら、指摘していただけると幸いです。

最後まで読んでいただきありがとうございました!!

参考

MDN

Polyfill (ポリフィル)
Falsy (偽値)
Array.prototype.map()
Array.prototype.filter()
Array.prototype.find()
Array.prototype.findIndex()
Array.prototype.every()
Array.prototype.some()
Array.prototype.reduce()
Array.prototype.sort()
Array.prototype.includes()
Promise

Qiita

Node.jsとはなにか?なぜみんな使っているのか?
Promiseの使い方、それに代わるasync/awaitの使い方

りあクト

りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅰ. 言語・環境編】

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

inputのtype別に、puppeteerでの入力方法をまとめた

inputタグに設定できるtype属性は今のところ、以下の20種類

button, checkbox, color, date, datetime-local, email, file, hidden, image, month, number, password, radio, search, submit, tel, text, time, url, week

puppeteerで簡単に入力できるもの、専用の入力方法が用意されているの、クセが強いもの、など様々だったのでまとめた。
なお、本ページの例は全タグにidがふってある優しい世界なので、要素の特定は別途頑張ってください。

確認環境

  • Windows10 2004
  • node v14.15.4
  • puppeteer 5.5.0
  • Chromium 90.0.4400.0

text系

以下のtypeを、まとめてtext系と呼ぶことにする。

email, number, password, search, tel, text, url

htmlだとこんなかんじ

<input type="text" id="text1">
<input type="email" id="email1">
<input type="search" id="search1">
<input type="tel" id="tel1">
<input type="url" id="url1">
<input type="number" id="number1">
<input type="password" id="password1">

submit時にvalidationがかかるもの、入力した文字が見えないものなど、それぞれ違いはあるが、入力する上では全てtype()を利用すれば良い

await page.type("#text1", "text");
await page.type("#email1", "example@example.com");
await page.type("#search1", "search text");
await page.type("#tel1", "09012345678");
await page.type("#url1", "https://example.com/");
await page.type("#number1", "42");
await page.type("#password1", "yourpassword");

button系

以下のtypeを、まとめてbutton系と呼ぶことにする。

button, image, reset, submit

htmlだと以下

<input type="reset" id="reset1">
<input type="button" id="button1" value="ボタン">
<input type="submit" id="submit1">
<input type="image" id="image1" src="image.png" alt="image-alt-text">

それぞれ役割があるが、入力(クリック)はどれもclick()でOK

await page.click("#button1");
await page.click("#image1");
await page.click("#reset1");
await page.click("#submit1");

radio, checkbox

ラジオボタンやチェックボックスも、素直にclick()でOK

<input type="radio" id="radio1">
<input type="checkbox" id="checkbox1">
await page.click("#radio1");
await page.click("#checkbox1");

file

<input type="file" id="file1">

type="file"には、専用のメソッドであるwaitForFileChooser()が用意されている。
使い方は、waitForNavigation()と同様に、クリック前に実行しておき、resolve後、ファイルを指定する。headlessでなくてもファイル選択のUIは表示されない。

const [fileChooser] = await Promise.all([
    page.waitForFileChooser(),
    page.click('#file1'),
]);
await fileChooser.accept(["/path/to/file"]);

もしくは、以下でも可。個人的にはネストが深くならないこちらのほうが好み。読みやすさは大差ない。

const fileChooserPromise = page.waitForFileChooser();
await page.click('#file1');
await (await fileChooserPromise).accept(["/path/to/file"]);

range

スライドバーが表示されるtype="range"

image.png

<input type="range" id="range1">

単純な値指定は難しいため、focus()でフォーカスを合わせて1右矢印キー左矢印キーを適切な回数押すと良い。(1回でstep属性の値だけ変化、デフォルト1)

await (await page.$("#range1")).focus();
for(let i = 0; i < 20; i++) {
    await page.keyboard.press("ArrowRight");
}

color

<input type="color" id="color1">

puppeteerの闇を感じることができるtype="color"。
クリックするとカラーピッカーが表示されるが、puppeteerに専用のメソッドは用意されていない2ため、このカラーピッカーを操作する必要がある。

image.png

やってみた結果が以下。

await (await page.$("#color1")).click();
await page.waitForTimeout(300)
await page.keyboard.press("Tab")
await page.keyboard.press("Tab")
await page.keyboard.press("Tab")
await page.keyboard.type("255")
await page.keyboard.press("Tab")
await page.keyboard.type("0")
await page.keyboard.press("Tab")
await page.keyboard.type("0")
await page.keyboard.press("Enter")

まずクリックでカラーピッカーを表示させ、タブを3回押すことでRGB値の入力欄に移動。
タブで移動しながらR,G,Bそれぞれの値を入れることで入力している。

クリック後300ms待っているのは、カラーピッカーが表示されるまでに若干時間がかかるため。ここは環境によって変える必要があるだろう。
DOM要素ならwaitForSelector()などで待つことができるが、カラーピッカーはDOM要素でない(・・・よね?)ため仕方なくwaitForTimeout()を利用している。

もうお気づきだろうと思うが、完全にChromiumのカラーピッカーのUIに依存した書き方になっているため、Firefoxではおそらく動かない(未検証)し、今後Chromiumのバージョンアップで動作しなくなる可能性もある。闇だ。

なお、puppeteerでは、ページ上でJavaScriptを動作させることもできるため、以下の書き方もできる。これなら1行だ。

await (await page.$("#color1")).evaluate((node) => {node.value = "#FF0000"});

ただ、この書き方だと、changeイベントが発火しないため、テスト内容によっては利用できない。

date系

以下のtypeを、まとめてdate系と呼ぶことにする。

date, datetime-local, month, time, week

image.png

htmlでは以下。

<input type="date" id="date1">
<input type="datetime-local" id="datetime-local1">
<input type="month" id="month1">
<input type="time" id="time1">
<input type="week" id="week1">

まず、入力ボックスの右側のカレンダーや時計をクリックするとピッカーが表示されるが、これを使おうとしてはいけない。マウスでしか開けない(多分)し、ピッカーはDOM要素でないため、開いた後の操作が難しい。ブラウザごとの差異も激しい。
キーボードで日付/時刻を入力するのが最善だ。

その上で、以下の点に気をつける必要がある。3

  1. 項目が一意に特定可能になると、次の項目に自動で遷移する
    • 「月」に5を入力すると自動で「日」にフォーカスが移るが、1を入力してもフォーカスはそのまま(10-12月があるため)
  2. 最大275760年9月13日まで入力できる4
    • つまり「年」に4桁入力しても、「月」にフォーカスを移してくれない
  3. min,max属性の指定によっては、入力不可な項目が出てくる
    • type="date"で、min="2020-01-01" max="2020-12-31"の場合、「年」は2020に固定され、フォーカスが当たるといきなり「月」の入力になる。
  4. ロケールによって年月日の順番が異なる

1.の解決策は2つ。
1つ目は、タブや右矢印などでフォーカスを移すこと。
「月」に1と入力したあとでタブを押せば、フォーカスが「日」に移ってくれる。
ただし、puppeteer上では月をtype()したあと、月が1のときのみタブをpress()してまた日をtype()する、という少し面倒な書き方になる。5
おすすめは次の2つ目だ。「月」を0埋めし、必ず2桁入力する。puppeteerのコードとしては、タブを押すよりだいぶ簡単になる。[^js_padding]

2.はほとんど1.と同種の問題だ。
「年」を入力したあと、タブでフォーカスを移してもよいが「年」の頭に00を加えて6桁入力するのが楽だ。type()のみですむ。
また、自動化対象のコードを変更できる場合、maxを指定してしまっても良い。max="9999-12-31"としておく6と、4桁入力した時点で「月」にフォーカスが移ってくれる。あと7900年後くらいまでは困る人もいないだろう。

3.は少し厄介だ。
「年」が入力不可だと分かっているのなら単純に「月」と「日」だけ入力すればよい(dateの場合)のだが、例えば「現在の日付から半年後まで」という仕様の場合、1-6月と7-12月で年の入力可否が変わってしまう。「1年後の前日の日付まで」という仕様なら1月1日のみCIが落ちるかもしれない。7
解決策としては、minとmax属性を読み込んで場合分け、が愚直な方法だろうか。
尤も、自動テストという文脈なら、テストの外部要因(時刻)でテスト内容や結果が変わるテストはイマイチなので、現在時刻をDIできる設計にするのが望ましい。8

4.は解決策を調査中である。ブラウザ上で、年月日がどの順番で表示されているのか、を取得する方法があれば知りたい。
もしくは、Chromiumの起動オプションでロケールを指定して年月日の順番を固定させる方法が利用できるかもしれない。(未調査)

ということを踏まえて、入力するコードは以下となる。(3と4は検討外)

await page.type("#date1", "0020210127");
await page.type("#datetime-local1", "00202101270812");
await page.type("#month1", "00202012");
await page.type("#time1", "0212");
await page.type("#week1", "00201943");

3や4が問題になる場合、changeイベントが発火しない、などの差分が許容できるなら、以下の書き方も利用できる。

await (await page.$("#date1")).evaluate((node) => {node.value = "2021-01-27"});
await (await page.$("#datetime-local1")).evaluate((node) => {node.value = "2021-01-27T08:12"});
await (await page.$("#month1")).evaluate((node) => {node.value = "2020-12"});
await (await page.$("#time1")).evaluate((node) => {node.value = "02:12"});
await (await page.$("#week1")).evaluate((node) => {node.value = "2019-W43"});

hidden

hiddenは普通はpuppeteerから基本的に書き換えないし、書き換えられない。
どうしてもやりたければ、evaluate()でやるしかない。ブラウザ自動操作の枠を超えているような気もするが。

<input type="hidden" id="hidden1">
await (await page.$("#hidden1")).evaluate((node) => {node.value = "hidden-value"});

おまけ

inputとともによく使われる以下についても簡単に。

textarea tag

<textarea id="textarea_tag1"></textarea>

textareaは、text系と同じで、type()でOK

await page.type("#textarea_tag1", "textarea");

button tag

<button id="button_tag1">button</button>

ボタンは、名前の通りbutton系、click()でOK

await page.click("#button_tag1");

select tag

<select id="select1">
    <option value="1" id="select1_1">option_1</option>
    <option value="2" id="select1_2">option_2</option>
    <option value="3" id="select1_3">option_3</option>
</select>

<select id="select_multi1" multiple>
    <option value="1" id="select_multi1_1">option_1</option>
    <option value="2" id="select_multi1_2">option_2</option>
    <option value="3" id="select_multi1_3">option_3</option>
</select>

image.png

selectは、専用のメソッドが用意されている。select()だ。
selectはmultipleか否かでUIが大きく異なるがselect()は両方に対応している。
第2引数以降に選択するoptionのvalueを指定する。

await page.select("#select1", "2");
await page.select("#select_multi1", "2", "3");

  1. focus()ではなく、click()をすると、値が変わることがある(中央付近をクリックしているような挙動)ため、不可。 

  2. issueとなってはいるが、進展なさそう 

  3. この注意点も現在のChromiumの実装に依存している。他のブラウザでは状況は異なるだろうし、Chromiumのバージョンアップでも変わる可能がある 

  4. 1970/1/1から1億日らしい 

  5. ここでは「月」のみの説明をしているが、「年」「時」でも同様の問題は起こり得る 

  6. もちろん、もっと妥当なmaxがあるのなら、それを記載するのが望ましい 

  7. ここでは「年」のみの説明をしているが、「月」「日」「時」でも同様の問題は起こり得る 

  8. 一例: https://qiita.com/suin/items/bcd7488df4403a53d7d9 

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

Node.js + Hapi + Couchbase ~ Node.js + NoSQL(Couchbase) アプリ開発 ステップバイステップガイド (3)

はじめに

本シリーズの前回の記事では、WEBアプリケーション・フレームワークとして、Expressを用いました。
今回は、Hapiを使った例を紹介します。

利用バージョン

  • Couchbase Server: 6.6
  • Node.js: 15.4.0
  • Node.js モジュール
    • couchbase: 3.1.1
    • hapi: 18.1.0
    • uuid: 8.3.2

実装

Couchbase Server

以下、前提事項を記します。

バケット

利用するバケット(MyFirstBucket)が存在している

インデックス

検索で利用する項目に対して、インデックスが作成されている。

CREATE INDEX def_index_type ON MyFirstBucket(type)

ユーザー

利用するバケットに対するアクセス権のあるユーザー(Administrator)を利用する。

Node.js

ライブラリインストール

npm install couchbase hapi uuid --save

ソースコード

app.js
const Hapi = require("hapi");
const Couchbase = require("couchbase");
const UUID = require("uuid");

const server = Hapi.Server({"host":"localhost","port":3000,routes:{cors: {origin: ['*']}}});

var cluster = new Couchbase.Cluster(
    'couchbase://localhost',
    {
      username: 'Administrator',
      password: 'password'
    }
  );

const bucket = cluster.bucket("MyFirstBucket");
const collection = bucket.defaultCollection();
const typeName = "user";
const statement = "SELECT `"+bucket._name+"`.* FROM `"+bucket._name+"` WHERE type = '"+typeName+"'";

const selectUsers = async (key) => { 
  const result = await cluster.query(statement);
  return result.rows;
}

const upsertUser = async (doc) => {
  try {
    const key = UUID.v4();
    const result = await collection.upsert(key, doc);
  } catch (error) {
    console.error(error);
  }
};

const init = async() => {
  await server.start(error=>{
      if(error){
        throw error;
      }
    });
  return server;
};

init().then(server => {
  console.log('Server running at:', server.info.uri);
}).catch(err => {
  console.log(err);
});

server.route({
    method:"GET",
    path:"/users",
    handler: async (request,h)=>{
      const rows = await selectUsers();
      return h.response(rows);
    }
});

server.route({
    method:"POST",
    path:"/user",
    handler: async (request, h)=>{
      try {
        request.payload["type"] = typeName;
        const result = await collection.upsert(UUID.v4(),request.payload);
        return h.response(request.payload);
      } catch (error) {
        console.error(error);
        return h.response("").code(500);
      }
    }
});

実行確認

サーバー起動

$ node app.js

アクセス結果

image.png

最後に

次のステップとして、Veu.jsを用いたフロントエンド・アプリケーションとの連携を予定しています(今回作成したPOSTのAPI /userについては、そちらで利用例を紹介します)。

参考情報

Hapiは、バージョン17で、APIの大きな変更が行われています。新しいAPIについて、下記の記事を参考にしました(この記事では、データベースとしてMongoDBが使われています)。
Developing RESTful APIs with Hapi

Couchbase SDKも、メジャーアップデートにより、APIが大きく変更されています。
過去に発表された下記の二つの連続した記事では、古いバージョンが用いられていますが、基本構成の参考にしています。

Create a RESTful API with Node.js, Hapi, and Couchbase NoSQL

Going Full Stack with Node.js, Vue.js, and Couchbase NoSQL

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

MySQL に ssl 認証で接続

次のファイルがあるフォルダーで実行します。

client-cert.pem
client-key.pem
server-ca.pem

コマンド

ssl_connect.sh
host="example.com"
user="scott"
pass="secret"
#
mysql --ssl-ca=server-ca.pem --ssl-cert=client-cert.pem \
        --ssl-key=client-key.pem \
        --host=${host} --user=${user} --password=${pass}

Python3

show_tables.py
#! /usr/bin/python
#
#   show_tables.py
#
#                       Jan/28/2021
import pymysql.cursors
#
host_aa='example.com' 
user_aa='scott'
pass_aa='secret'
db_aa='dbfirst'
#
connection = pymysql.connect(host=host_aa,
    user=user_aa,
    password=pass_aa,
    db=db_aa,
    charset='utf8',
    cursorclass=pymysql.cursors.DictCursor,
    ssl={'key': './client-key.pem', 'cert': './client-cert.pem', 'ca': './server-ca.pem','check_hostname': False})
cursor = connection.cursor()
cursor.execute("show tables")
result = cursor.fetchall()
# print(result)
for rr in result:
    print(rr)
cursor.close()
connection.close()

Node.js

show_tables.js
#! /usr/bin/node
// ---------------------------------------------------------------
//  show_tables.js
//
//                  Jan/28/2021
//
// ---------------------------------------------------------------
'use strict'

var fs = require("fs")
// ---------------------------------------------------------------
console.error ("*** 開始 ***")

var mysql = require('mysql')

var connection = mysql.createConnection ({
    host: 'example.com',
    user: 'scott',
    password: 'secret',
    database : 'dbfirst',
ssl      : {
        ca   : fs.readFileSync('./server-ca.pem'),
        key  : fs.readFileSync('./client-key.pem'),
        cert : fs.readFileSync('./client-cert.pem'),
  }
    })

connection.query("show tables", function (err, rows)
    {
    if (err) throw err
    console.log (rows.length)

    rows.forEach(function(row)
        {
        console.log(row)
        })


    connection.end()
    console.error ("*** 終了 ***")
    })

// ---------------------------------------------------------------

Go

show_tables.go
// ----------------------------------------------------------------
//
//  show_tables.go
//
//                  Jan/29/2021
//
// ----------------------------------------------------------------
package main

import (
    "crypto/tls"
    "database/sql"
    "fmt"
    "log"
    "os"

    "github.com/go-sql-driver/mysql"
)

func main() {
    fmt.Fprintf (os.Stderr,"*** 開始 ***\n")
    cert, err := tls.LoadX509KeyPair("./client-cert.pem", "./client-key.pem")
    if err != nil {
        log.Fatal(err)
    }
    clientCert := []tls.Certificate{cert}

    mysql.RegisterTLSConfig("custom", &tls.Config{
        Certificates:       clientCert,
        InsecureSkipVerify: true,
    })

    db, err := sql.Open("mysql", "scott:secret@tcp(example.com:3306)/dbfirst?tls=custom")
    if err != nil {
        log.Fatal(err)
    }

    err = db.Ping()
    if err != nil {
        log.Fatal(err)
    }

sql_str := "show tables"
rows, err := db.Query(sql_str)
if err != nil {
        fmt.Println(err)
}
defer rows.Close()

for rows.Next() {
        var name string
    if err := rows.Scan(&name); err != nil {
                fmt.Println(err)
        }
        fmt.Printf ("%s\n",name)
}

if err := rows.Err(); err != nil {
        fmt.Println(err)
        }

    fmt.Fprintf (os.Stderr,"*** 終了 ***\n")
}

// ----------------------------------------------------------------

参考ページ
MySQLでクライアント証明書を使う

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

CognitoのJWTをNode.js(Typescript)で検証する方法

JWT検証の実装

npm i jsonwebtoken jwks-rsa
npm i -D @types/jsonwebtoken
index.ts
import jwt, { JwtHeader, SigningKeyCallback } from "jsonwebtoken";
import jwksClient from "jwks-rsa";
var client = jwksClient({
  jwksUri:
    "https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json",
});

function getKey(header: JwtHeader, callback: SigningKeyCallback) {
  if (!header.kid) throw new Error("not found kid!");
  client.getSigningKey(header.kid, function (err, key) {
    if (err) throw err;
    callback(null, key.getPublicKey());
  });
}

const token = "{jwtToken}";

jwt.verify(token, getKey, function (err, decoded) {
  if (err) throw err;
  console.log(decoded);
});
npx ts-node index.ts

キャッシュについて

毎回Cognitoのjwksにアクセスしなくて良いように、デフォルトでキャッシュ有効になってます。便利!
https://github.com/auth0/node-jwks-rsa#caching

AmplifyでのJWTの取り方

import { Auth } from "aws-amplify";
.....
const session = await Auth.currentSession()
const jwt = session.getAccessToken().getJwtToken()
console.log(jwt);

JWTってJSON Web Tokenの略だから、このメソッド名Token被ってない!?

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

Lambda(Node.js)でタイムアウト指定できるfetch

request-promiseは非推奨ですよって話

request-promiseに依存しているrequestパッケージが非推奨になったことから芋づる式に影響するみたいです。
https://github.com/request/request/issues/3142

fetch使おう

以下のパッケージでfetchを使います
https://www.npmjs.com/package/node-fetch

タイムアウトするためのざっくりとした流れ

AbortControllerという非同期処理を中断させるインターフェースを使用します。

タイムアウトさせたい非同期処理にAbortControllerインスタンスのsignalを渡して、setTimeout()で指定時間にabort()を外から呼ぶことで中断させるといった感じです。(Goのcontextと似てますね)

サンプルうまくいかない問題

READMEの通りLambdaで動かしてみましたが、うまくabort()が動きませんでした。

npmのサイトには以下のようにバージョンは>= v8.0.0とあったので、Lambda(nodejs12.x)でもいけると思ったんですが、、

NOTE: You may cancel streamed requests only on Node >= v8.0.0

const AbortController = require('abort-controller');
const controller = new AbortController();
const timeout = setTimeout(() => {
    controller.abort(); // ここが空になる
}, 150);


try {
    const response = await fetch('https://example.com', {signal: controller.signal});
    const data = await response.json();
} catch (error) {
    // errorが空で返るのでAbortErrorを判定できない
    if (error instanceof fetch.AbortError) {
        console.log('request was aborted');
    }
} finally {
    clearTimeout(timeout);
}

最終的にLambdaで動いたタイムアウト指定できるfetch

以下のパッケージで生成したAbortControllerが動きました
https://www.npmjs.com/package/node-abort-controller

async function postWithTimeout(url, body, ms) {
  const fetch = require("node-fetch");
  const AbortController = require("node-abort-controller");

  const controller = new AbortController();
  const timeout = setTimeout(() => {
    controller.abort();
  }, ms);

  const options = {
    method: "post",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
    signal: controller.signal, // シグナルを渡しておく
  };

  try {
    const response = await fetch(url, options);
    const body = await response.json();
    return JSON.stringify(body);
  } catch (error) {
    if (error.type == "aborted") {
      console.log("the user aborted a request");
      throw new Error(JSON.stringify({ type: "abort" }));
    }

    throw new Error(JSON.stringify({ type: "other" }));
  } finally {
    clearTimeout(timeout);
  }
}

まとめ

request-promiseからの切り替えをお考えのあなたに向けたfetchな記事になれば幸いです

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

nvm for windowsの設定手順の備忘録

はじめに

  • Windows環境上で、Nodejsの開発環境を構築する機会があったので、その備忘録です。
  • Node.jsの管理ツールとして、nvm for windowsを使用しました。

nvm for windowsのインストール

  • nvm for windowsのインストーラをダウンロードし、インストールします(全てデフォルト設定でOK)。 image.png

Node.jsのインストール

  • コマンドプロンプトを起動し、下記のコマンドを実行します。実行後、インストール可能なバージョンが一覧表示されます。
cmd
# インストール可能なバージョンを表示
$ nvm list available

WS000000.JPG

  • バージョンを指定して、インストールします。
cmd
# v10.16.2をインストール
$ nvm install 10.16.2
$ node --vesion
# v10.16.2
  • 使用中のバージョンを確認します。
cmd
$ nvm list
# *10.16.2
  • 別バージョンをインストールしてます。
cmd
# v12.13.1をインストール
$ nvm install 12.13.1
  • インストール済みバージョンを確認します。
cmd
$ nvm list
#   12.13.1
#  *10.16.2
# ⇒ v10.16.2を使用中
  • バージョンを切り替えます。
cmd
# バージョン指定
$ nvm use 12.13.1
$ nvm list
#  *12.13.1
#   10.16.2
$ node  --version
# v12.13.1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Windows】Node.js 環境構築手順の備忘録

はじめに

  • Windows環境上で、Nodejsの開発環境を構築する機会があったので、その備忘録です。
  • Node.jsの管理ツールとして、nvm for windowsを使用しました。

nvm for windowsのインストール

  • nvm for windowsのインストーラをダウンロードし、インストールします(全てデフォルト設定でOK)。 image.png

Node.jsのインストール

  • コマンドプロンプトを起動し、下記のコマンドを実行します。実行後、インストール可能なバージョンが一覧表示されます。
cmd
# インストール可能なバージョンを表示
$ nvm list available

WS000000.JPG

  • バージョンを指定して、インストールします。
cmd
# v10.16.2をインストール
$ nvm install 10.16.2
$ node --vesion
# v10.16.2
  • 使用中のバージョンを確認します。
cmd
$ nvm list
# *10.16.2
  • 別バージョンをインストールしてます。
cmd
# v12.13.1をインストール
$ nvm install 12.13.1
  • インストール済みバージョンを確認します。
cmd
$ nvm list
#   12.13.1
#  *10.16.2
# ⇒ v10.16.2を使用中
  • バージョンを切り替えます。
cmd
# バージョン指定
$ nvm use 12.13.1
$ nvm list
#  *12.13.1
#   10.16.2
$ node  --version
# v12.13.1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Windows】Nodeインストール手順の備忘録

はじめに

  • Windows環境上で、Nodeの開発環境を構築する機会があったので、その備忘録です。
  • Nodeの管理ツールとして、nvm for windowsを使用しました。

nvm for windowsのインストール

  • nvm for windowsのインストーラをダウンロードし、インストールします(全てデフォルト設定でOK)。 image.png

Node.jsのインストール

  • コマンドプロンプトを起動し、インストール可能なバージョンを確認します。
cmd
# インストール可能なバージョンを表示
$ nvm list available

WS000000.JPG

  • バージョンを指定して、インストールします。
cmd
# v10.16.2をインストール
$ nvm install 10.16.2
$ node --vesion
# v10.16.2
  • 使用中のバージョンを確認します。
cmd
$ nvm list
# *10.16.2
  • 別バージョンをインストールします。
cmd
# v12.13.1をインストール
$ nvm install 12.13.1
  • インストール済みバージョンを確認します。
cmd
$ nvm list
#   12.13.1
#  *10.16.2
# ⇒ v10.16.2を使用中
  • バージョンを切り替えます。
cmd
# バージョン指定
$ nvm use 12.13.1
$ nvm list
#  *12.13.1
#   10.16.2
$ node  --version
# v12.13.1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node + expressでJWT (2021年1月)

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

準備

作業場作成

ひとまず作業場所を確保し、必要なモジュールをインストール。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
var express = require('express');
var app = express();
var jwt = require('jsonwebtoken');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.listen(3000, function () {
    console.log("App start on port 3000");
})

//認証無しAPI
app.get('/', function (req, res) {
    res.json({ status: "OK" });
})

//認証+Tokenの発行
app.post('/login', function (req, res) {

    //ID,PW取得
    var username = req.body.username;
    var password = req.body.password;

    //認証
    //実際はDB等と連携
    if (username === "hoge" && password === "password") {
        //token生成(フォマットは適当だが、有効期限を設定)
        const token = jwt.sign({ username: username }, 'my_secret', { expiresIn: '1h' });
        res.json({
            token: token
        });
    } else {
        res.json({
            error: "auth error"
        });
    }

})

//認証有りAPI
app.get('/protected', verifyToken, function (req, res) {
    res.send("Protected Contents");
})

function verifyToken(req, res, next) {
    const authHeader = req.headers["authorization"];
    //HeaderにAuthorizationが定義されているか
    if (authHeader !== undefined) {
        //Bearerが正しく定義されているか
        if (authHeader.split(" ")[0] === "Bearer") {
            try {
                const token = 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/

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

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

本記事では、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/

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