20191203のNode.jsに関する記事は20件です。

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

supertestとは

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

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

passport-stubとは

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

テストの例

test.js
//supertestの読み込み
const request = require('supertest');
//supertestで使う、app.jsの読み込み
const app = require('app');
//passport-stubの読み込み
const passportStub = require('passport-stub');

//ログイン(/login)のテストであることを明示
describe('/login', () => {
  //before、afterはmochaの機能
  before(() => {
    //テストの前にpassportstubモジュールでログイン
    passportStub.install(app);
    //'testuser'としてログイン
    passportStub.login({ username: 'testuser' });
  });
  after(() => {
    //テストの後にpassportstubモジュールでログアウト
    passportStub.logout();
    passportStub.uninstall(app);
  });

//以下の記法は、supertestの記法
 //テストの内容を指定
  it('ログインのためのリンクが含まれる', (done) => {
    //request(app).get('/login') で、 /login への GETリクエストを作成
    request(app)
      .get('/login')
      //文字列を2つ引数として渡すとヘッダのテスト
      .expect('Content-Type', 'text/html; charset=utf-8')
      //正規表現を1つ渡すとHTMLのテスト
      .expect(/<a href="\/auth\/facebook"/)
      //期待されるステータスコードの整数と、テスト自体の引数に渡されるdone 関数を渡すと、レスポンスヘッダのテスト
      .expect(200, done);
  });

  it('ログイン時はユーザー名が表示される', (done) => {
    request(app)
      .get('/login')
      .expect(/testuser/)
      .expect(200, done);
  });
});

//ログアウト(/logout)のテストであることを明示
describe('/logout', () => {
 //テスト内容を明示
  it('ログアウト後に / にリダイレクトされる', (done) => {
    ////request(app).get('/logout')で、/logoutへのGETリクエストを作成
    request(app)
      .get('/logout')
       // `/`へリダイレクトされるかのテスト
      .expect('Location', '/')
    // ステータスコードがリクエストであるかのテスト
      .expect(302, done);
  });
});

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

テスト結果

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

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

decoratorとライブラリを紐解き、軽量 DI コンテナを自作しよう

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

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

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

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

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

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

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

decorator ベース

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

interface に対応

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

interface IProps {
  isLoading: boolean;
  data: IUser;
  error: string;
}

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

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

class DBRepository implements IRepository {
  getAll() {
    // DBにアクセス
  }
}

class APIRepository implements IRepository {
  getAll() {
    // APIにアクセス
  }
}

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

class UserService {
  private readonly repository: IRepository;

  constrcutor(repo: IRepository) {
    this.repository = repo;
  }

  getAllUser() {
    return this.repository.getAll();
  }
}

// DBRepository も APIRepository も同じ IRepository の実装なので、両方とも UserService に injection できる
const serviceA = new UserService(new DBRepository());
const serviceB = new UserService(new APIRepository());

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

簡単には満たせない要求

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

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

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

decorator

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

// decorator
function uriEncoded(target: any, propKey: string, desc: PropertyDescriptor) {
  const method = desc.value;
  desc.value = function() {
    const res = Reflect.apply(method, this, arguments);
    if (typeof res === "string") {
      return encodeURIComponent(res);
    }
    return res;
  };
}

class Sample {
  @uriEncoded
  hoge(): string {
    return "こんにちは";
  }
}

console.log(new Sample().hoge());
// 出力
// %E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF

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

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

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

Reflection Metadata API

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

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

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  console.log(constructor);
  return class extends constructor {};
}

@classDecorator
class Hoge {
  constructor(hoge: string) {}
}

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

Reflection

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

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  console.log(Reflect.get(Hoge, "constructor"));
  console.log(Reflect.construct(Hoge, []));
  return class extends constructor {};
}

@classDecorator
class Hoge {
  hoge: string;
  constructor(hoge: string) {
    console.log("hey");
  }
}

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

reflect-metadata

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

background

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

goals

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

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

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

getMetadata

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

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

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

defineMetadata

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

getOwnMetadata

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

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

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

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

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

function injectable<T>(): (target: constructor<T>) => void {
  return function(target: constructor<T>): void {
    typeInfo.set(target, getParamInfo(target));
  };
}

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

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

  return params;
}

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

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

interface への依存を登録(inject)

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

function inject(
  token: InjectionToken<any>
): (target: any, propertyKey: string | symbol, parameterIndex: number) => any {
  return defineInjectionTokenMetadata(token);
}

export function defineInjectionTokenMetadata(
  data: any
): (target: any, propertyKey: string | symbol, parameterIndex: number) => any {
  return function(
    target: any,
    _propertyKey: string | symbol,
    parameterIndex: number
  ): any {
    const injectionTokens =
      Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};
    injectionTokens[parameterIndex] = data;
    Reflect.defineMetadata(
      INJECTION_TOKEN_METADATA_KEY,
      injectionTokens,
      target
    );
  };
}

inject が必要になる背景

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

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

class DBRepository implements IRepository {
  getAll() {
    // DBにアクセス
  }
}

class APIRepository implements IRepository {
  getAll() {
    // APIにアクセス
  }
}

@injectable()
class UserService {
  private readonly repository: IRepository;

  constrcutor(repo: IRepository) {
    this.repository = repo;
  }

  getAllUser() {
    return this.repository.getAll();
  }
}

const serviceA = new UserService(new DBRepository());
const serviceA = new UserService(new APIRepository());

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

@injectable()
class UserService {
  private readonly repository: IRepository;

  constrcutor(@inject("IDBRepository") repo: IRepository) {
    this.repository = repo;
  }

  getAllUser() {
    return this.repository.getAll();
  }
}

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

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

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

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

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

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

Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY, injectionTokens, target);

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

依存を解決(resolve)

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

コンテナ

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

class InternalDependencyContainer implements DependencyContainer {
  // 300行くらい続く
}

依存の解決

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

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

public resolve<T>(
    token: InjectionToken<T>,
    context: ResolutionContext = new ResolutionContext()
  ): T {
    const registration = this.getRegistration(token);

    // - 中略 -

    return this.construct(token as constructor<T>, context);
  }

  private construct<T>(ctor: constructor<T>, context: ResolutionContext): T {
     // - 中略 -

    const paramInfo = typeInfo.get(ctor);

    // - 中略 -

    const params = paramInfo.map(param => {
        // - 中略
      return this.resolve(param, context);
    });

    return new ctor(...params);
  }

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

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

private resolveRegistration<T>(
    registration: Registration,
    context: ResolutionContext
  ): T {

    // - 中略 -

    if (registration.options.lifecycle === Lifecycle.ResolutionScoped) {
      context.scopedResolutions.set(registration, resolved);
    }

    return resolved;
  }

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

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

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

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

class Container {
  private static instance: Container;

  data: Map<constructor<any>, constructor<any>[]>;
  context: Map<constructor<any>, constructor<any>>;

  private constructor() {
    this.data = new Map<constructor<any>, constructor<any>[]>();
    this.context = new Map<any, constructor<any>>();
  }

  static getInstance() {
    if (!Container.instance) {
      Container.instance = new Container();
    }
    return Container.instance;
  }
}

export default Container;

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

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

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

class Container {
  // -中略-

  public register(constructor: constructor<any>, depends: constructor<any>[]) {
    this.data.set(constructor, depends);
  }
}

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

interface を経由しない場合

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

export const injectable = (): ClassDecorator => {
  return target => {
    const params: any[] =
      Reflect.getMetadata("design:paramtypes", target) || [];
    Container.getInstance().register(target, params);
  };
};

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

interface を経由させる場合

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

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

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

export const inject = (
  token: InjectionToken<any>
): ((
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number
) => any) => {
  return defineInjectionTokenMetadata(token);
};

const defineInjectionTokenMetadata = (data: any): ((target: any) => any) => {
  return function(target: any): any {
    const interfaceName: any = {};
    interfaceName[0] = data;
    Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY, interfaceName, target);
  };
};

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

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

Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY, interfaceName, target);

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

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

export const injectable = (): ClassDecorator => {
  return target => {
    const params: any[] =
      Reflect.getMetadata("design:paramtypes", target) || [];

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

    Container.getInstance().register(target, params);
  };
};

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

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

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

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

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

public resolve(ctor: constructor<any>) {
  // 受け取った依存を注入するために依存をインスタンス化する関数を呼び出す(できないときもある)
  this.resolveInstance(ctor);

  // resolveしたいクラスの依存を取得する
  const dependantClasses = this.data.get(ctor);

  // その依存のインスタンスを全て取得する(この時点で全依存はインスタンスされている想定)
  if (!dependantClasses) return;
  const instances = dependantClasses.map(cls => this.context.get(cls));

  // 依存を全て注入してインスタンス化
  return new ctor(...instances)
}

public resolve(ctor: constructor<any>) {

  // 注入しなければいけない依存のコンストラクタを取得
  const targetDependencies = this.data.get(ctor);
  if (targetDependencies && targetDependencies.length > 0) {
    // 注入しなければいけない依存がなければ即時インスタンス化
    const instance = new ctor();
    this.context.set(ctor, instance);
  }

  // 注入しなければいけない依存をインスタンス化する
  //(引数が注入される側なのはI/Fとしてはいけてないです。すみません。)
  this.resolveInstance(ctor);
  const dependantClasses = this.data.get(ctor);

  // 必要な解決済み依存を取得
  const instances = dependantClasses.map(cls => this.context.get(cls));

  // 依存を全て注入してインスタンス化
  return new ctor(...instances);
}

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

private resolveInstance(ctor: any) {
  // 引数のコンストラクタをインスタンス化するために必要な依存を取得
  const depends = this.data.get(ctor);
  if (!depends) {
    // もし必要な依存がないなら、そのままコンストラクタをインスタンス化する
    const i = new ctor();
    this.context.set(ctor, i);
    return;
  }

  // 必要な依存あるなら、そのままinstance化できるまで resolveを再帰的に呼ぶ
  // 依存が一方向であることを前提にしているのでこう書いても最終的に依存を解決できる
  // (単独でインスタンス化できるコンストラクタにいつか出会えるから)
  this.resolve(depends[0]);

  // 必要な依存の全インスタンスを取得
  const dependInstances = depends.map(d => {
    return this.context.get(d);
  });

  // その依存を注入し保存する
  const instance = new ctor(...dependInstances);
  this.context.set(ctor, instance);
}

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

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

依存の階層が多い場合

// so many nest example

import { injectable } from "../main/Injectable";
import "reflect-metadata";
import Container from "../main/Container";

class A {
  call() {
    console.log("CALL A");
  }
}

@injectable()
class B {
  a: A;

  constructor(a: A) {
    this.a = a;
  }
}

@injectable()
class C {
  b: B;

  constructor(b: B) {
    this.b = b;
  }
}

@injectable()
class D {
  c: C;

  constructor(c: C) {
    this.c = c;
  }
}

@injectable()
class E {
  d: D;

  constructor(d: D) {
    this.d = d;
  }
}

const container = Container.getInstance();
const e = container.resolve(E);
e.d.c.b.a.call();

これを実行すると・・・

$ node dist/example/test1.js
CALL A

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

// can revolve via interface

import Container from "../main/Container";
import { injectable } from "../main/Injectable";
import "reflect-metadata";
import { inject } from "../main/inject";

interface IRepository {
  read: () => number[];
  create: (val: number) => void;
}

interface IStoreAdapter {
  read: () => number[]; // in real, should return a DTO.
  create: (val: number) => void;
}

class MemoryStoreImpl implements IStoreAdapter {
  private store: number[] = [];
  read() {
    return this.store;
  }

  create(val: number) {
    this.store.push(val);
  }
}

@injectable()
class DBRepositoryImpl implements IRepository {
  adapter: IStoreAdapter;

  constructor(@inject(MemoryStoreImpl) adapter: IStoreAdapter) {
    this.adapter = adapter;
  }

  read() {
    return this.adapter.read();
  }

  create(val: number) {
    this.adapter.create(val);
  }
}

@injectable()
class APIRepositoryImpl implements IRepository {
  read() {
    console.log("get data");
    return [1, 2, 3];
  }

  create(val: number) {
    console.log("post data");
  }
}

@injectable()
class Service {
  repo: IRepository;

  constructor(@inject(DBRepositoryImpl) repo: IRepository) {
    this.repo = repo;
  }

  public find() {
    return this.repo.read();
  }

  public save(val: number) {
    this.repo.create(val);
  }
}

// interface test

const container = Container.getInstance();
const service = container.resolve(Service);
service.save(1);
service.save(2);
const data = service.find();
console.log("the value: ", data);

これを実行すると・・・

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

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

import { injectable } from "../main/Injectable";
import "reflect-metadata";
import Container from "../main/Container";

class Hoge {
  call() {
    console.log("hogeeeeeeeeeeee");
  }
}

class Piyo {
  call() {
    console.log("piyooooooooooooo");
  }
}

@injectable()
class Fuga {
  hoge: Hoge;
  piyo: Piyo;

  constructor(hoge: Hoge, piyo: Piyo) {
    this.hoge = hoge;
    this.piyo = piyo;
  }
}

@injectable()
class Foo {
  fuga: Fuga;

  constructor(fuga: Fuga) {
    this.fuga = fuga;
  }
}

const container = Container.getInstance();
const foo = container.resolve(Foo);
foo.fuga.hoge.call();
foo.fuga.piyo.call();
$ node dist/example/test2.js
hogeeeeeeeeeeee
piyooooooooooooo

まとめ

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


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

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

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

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

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

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

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

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

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

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

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

decorator ベース

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

interface に対応

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

interface IProps {
  isLoading: boolean;
  data: IUser;
  error: string;
}

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

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

class DBRepository implements IRepository {
  getAll() {
    // DBにアクセス
  }
}

class APIRepository implements IRepository {
  getAll() {
    // APIにアクセス
  }
}

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

class UserService {
  private readonly repository: IRepository;

  constrcutor(repo: IRepository) {
    this.repository = repo;
  }

  getAllUser() {
    return this.repository.getAll();
  }
}

// DBRepository も APIRepository も同じ IRepository の実装なので、両方とも UserService に injection できる
const serviceA = new UserService(new DBRepository());
const serviceB = new UserService(new APIRepository());

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

簡単には満たせない要求

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

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

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

decorator

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

// decorator
function uriEncoded(target: any, propKey: string, desc: PropertyDescriptor) {
  const method = desc.value;
  desc.value = function() {
    const res = Reflect.apply(method, this, arguments);
    if (typeof res === "string") {
      return encodeURIComponent(res);
    }
    return res;
  };
}

class Sample {
  @uriEncoded
  hoge(): string {
    return "こんにちは";
  }
}

console.log(new Sample().hoge());
// 出力
// %E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF

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

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

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

Reflection Metadata API

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

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

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  console.log(constructor);
  return class extends constructor {};
}

@classDecorator
class Hoge {
  constructor(hoge: string) {}
}

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

Reflection

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

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  console.log(Reflect.get(Hoge, "constructor"));
  console.log(Reflect.construct(Hoge, []));
  return class extends constructor {};
}

@classDecorator
class Hoge {
  hoge: string;
  constructor(hoge: string) {
    console.log("hey");
  }
}

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

reflect-metadata

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

background

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

goals

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

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

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

getMetadata

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

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

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

defineMetadata

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

getOwnMetadata

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

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

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

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

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

function injectable<T>(): (target: constructor<T>) => void {
  return function(target: constructor<T>): void {
    typeInfo.set(target, getParamInfo(target));
  };
}

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

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

  return params;
}

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

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

interface への依存を登録(inject)

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

function inject(
  token: InjectionToken<any>
): (target: any, propertyKey: string | symbol, parameterIndex: number) => any {
  return defineInjectionTokenMetadata(token);
}

export function defineInjectionTokenMetadata(
  data: any
): (target: any, propertyKey: string | symbol, parameterIndex: number) => any {
  return function(
    target: any,
    _propertyKey: string | symbol,
    parameterIndex: number
  ): any {
    const injectionTokens =
      Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};
    injectionTokens[parameterIndex] = data;
    Reflect.defineMetadata(
      INJECTION_TOKEN_METADATA_KEY,
      injectionTokens,
      target
    );
  };
}

inject が必要になる背景

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

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

class DBRepository implements IRepository {
  getAll() {
    // DBにアクセス
  }
}

class APIRepository implements IRepository {
  getAll() {
    // APIにアクセス
  }
}

@injectable()
class UserService {
  private readonly repository: IRepository;

  constrcutor(repo: IRepository) {
    this.repository = repo;
  }

  getAllUser() {
    return this.repository.getAll();
  }
}

const serviceA = new UserService(new DBRepository());
const serviceA = new UserService(new APIRepository());

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

@injectable()
class UserService {
  private readonly repository: IRepository;

  constrcutor(@inject("IDBRepository") repo: IRepository) {
    this.repository = repo;
  }

  getAllUser() {
    return this.repository.getAll();
  }
}

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

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

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

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

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

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

Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY, injectionTokens, target);

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

依存を解決(resolve)

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

コンテナ

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

class InternalDependencyContainer implements DependencyContainer {
  // 300行くらい続く
}

依存の解決

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

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

public resolve<T>(
    token: InjectionToken<T>,
    context: ResolutionContext = new ResolutionContext()
  ): T {
    const registration = this.getRegistration(token);

    // - 中略 -

    return this.construct(token as constructor<T>, context);
  }

  private construct<T>(ctor: constructor<T>, context: ResolutionContext): T {
     // - 中略 -

    const paramInfo = typeInfo.get(ctor);

    // - 中略 -

    const params = paramInfo.map(param => {
        // - 中略
      return this.resolve(param, context);
    });

    return new ctor(...params);
  }

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

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

private resolveRegistration<T>(
    registration: Registration,
    context: ResolutionContext
  ): T {

    // - 中略 -

    if (registration.options.lifecycle === Lifecycle.ResolutionScoped) {
      context.scopedResolutions.set(registration, resolved);
    }

    return resolved;
  }

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

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

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

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

class Container {
  private static instance: Container;

  data: Map<constructor<any>, constructor<any>[]>;
  context: Map<constructor<any>, constructor<any>>;

  private constructor() {
    this.data = new Map<constructor<any>, constructor<any>[]>();
    this.context = new Map<any, constructor<any>>();
  }

  static getInstance() {
    if (!Container.instance) {
      Container.instance = new Container();
    }
    return Container.instance;
  }
}

export default Container;

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

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

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

class Container {
  // -中略-

  public register(constructor: constructor<any>, depends: constructor<any>[]) {
    this.data.set(constructor, depends);
  }
}

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

interface を経由しない場合

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

export const injectable = (): ClassDecorator => {
  return target => {
    const params: any[] =
      Reflect.getMetadata("design:paramtypes", target) || [];
    Container.getInstance().register(target, params);
  };
};

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

interface を経由させる場合

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

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

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

export const inject = (
  token: InjectionToken<any>
): ((
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number
) => any) => {
  return defineInjectionTokenMetadata(token);
};

const defineInjectionTokenMetadata = (data: any): ((target: any) => any) => {
  return function(target: any): any {
    const interfaceName: any = {};
    interfaceName[0] = data;
    Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY, interfaceName, target);
  };
};

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

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

Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY, interfaceName, target);

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

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

export const injectable = (): ClassDecorator => {
  return target => {
    const params: any[] =
      Reflect.getMetadata("design:paramtypes", target) || [];

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

    Container.getInstance().register(target, params);
  };
};

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

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

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

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

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

public resolve(ctor: constructor<any>) {
  // 受け取った依存を注入するために依存をインスタンス化する関数を呼び出す(できないときもある)
  this.resolveInstance(ctor);

  // resolveしたいクラスの依存を取得する
  const dependantClasses = this.data.get(ctor);

  // その依存のインスタンスを全て取得する(この時点で全依存はインスタンスされている想定)
  if (!dependantClasses) return;
  const instances = dependantClasses.map(cls => this.context.get(cls));

  // 依存を全て注入してインスタンス化
  return new ctor(...instances)
}

public resolve(ctor: constructor<any>) {

  // 注入しなければいけない依存のコンストラクタを取得
  const targetDependencies = this.data.get(ctor);
  if (targetDependencies && targetDependencies.length > 0) {
    // 注入しなければいけない依存がなければ即時インスタンス化
    const instance = new ctor();
    this.context.set(ctor, instance);
  }

  // 注入しなければいけない依存をインスタンス化する
  //(引数が注入される側なのはI/Fとしてはいけてないです。すみません。)
  this.resolveInstance(ctor);
  const dependantClasses = this.data.get(ctor);

  // 必要な解決済み依存を取得
  const instances = dependantClasses.map(cls => this.context.get(cls));

  // 依存を全て注入してインスタンス化
  return new ctor(...instances);
}

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

private resolveInstance(ctor: any) {
  // 引数のコンストラクタをインスタンス化するために必要な依存を取得
  const depends = this.data.get(ctor);
  if (!depends) {
    // もし必要な依存がないなら、そのままコンストラクタをインスタンス化する
    const i = new ctor();
    this.context.set(ctor, i);
    return;
  }

  // 必要な依存あるなら、そのままinstance化できるまで resolveを再帰的に呼ぶ
  // 依存が一方向であることを前提にしているのでこう書いても最終的に依存を解決できる
  // (単独でインスタンス化できるコンストラクタにいつか出会えるから)
  this.resolve(depends[0]);

  // 必要な依存の全インスタンスを取得
  const dependInstances = depends.map(d => {
    return this.context.get(d);
  });

  // その依存を注入し保存する
  const instance = new ctor(...dependInstances);
  this.context.set(ctor, instance);
}

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

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

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

依存の階層が多い場合

// so many nest example

import { injectable } from "../main/Injectable";
import "reflect-metadata";
import Container from "../main/Container";

class A {
  call() {
    console.log("CALL A");
  }
}

@injectable()
class B {
  a: A;

  constructor(a: A) {
    this.a = a;
  }
}

@injectable()
class C {
  b: B;

  constructor(b: B) {
    this.b = b;
  }
}

@injectable()
class D {
  c: C;

  constructor(c: C) {
    this.c = c;
  }
}

@injectable()
class E {
  d: D;

  constructor(d: D) {
    this.d = d;
  }
}

const container = Container.getInstance();
const e = container.resolve(E);
e.d.c.b.a.call();

これを実行すると・・・

$ node dist/example/test1.js
CALL A

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

// can revolve via interface

import Container from "../main/Container";
import { injectable } from "../main/Injectable";
import "reflect-metadata";
import { inject } from "../main/inject";

interface IRepository {
  read: () => number[];
  create: (val: number) => void;
}

interface IStoreAdapter {
  read: () => number[]; // in real, should return a DTO.
  create: (val: number) => void;
}

class MemoryStoreImpl implements IStoreAdapter {
  private store: number[] = [];
  read() {
    return this.store;
  }

  create(val: number) {
    this.store.push(val);
  }
}

@injectable()
class DBRepositoryImpl implements IRepository {
  adapter: IStoreAdapter;

  constructor(@inject(MemoryStoreImpl) adapter: IStoreAdapter) {
    this.adapter = adapter;
  }

  read() {
    return this.adapter.read();
  }

  create(val: number) {
    this.adapter.create(val);
  }
}

@injectable()
class APIRepositoryImpl implements IRepository {
  read() {
    console.log("get data");
    return [1, 2, 3];
  }

  create(val: number) {
    console.log("post data");
  }
}

@injectable()
class Service {
  repo: IRepository;

  constructor(@inject(DBRepositoryImpl) repo: IRepository) {
    this.repo = repo;
  }

  public find() {
    return this.repo.read();
  }

  public save(val: number) {
    this.repo.create(val);
  }
}

// interface test

const container = Container.getInstance();
const service = container.resolve(Service);
service.save(1);
service.save(2);
const data = service.find();
console.log("the value: ", data);

これを実行すると・・・

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

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

import { injectable } from "../main/Injectable";
import "reflect-metadata";
import Container from "../main/Container";

class Hoge {
  call() {
    console.log("hogeeeeeeeeeeee");
  }
}

class Piyo {
  call() {
    console.log("piyooooooooooooo");
  }
}

@injectable()
class Fuga {
  hoge: Hoge;
  piyo: Piyo;

  constructor(hoge: Hoge, piyo: Piyo) {
    this.hoge = hoge;
    this.piyo = piyo;
  }
}

@injectable()
class Foo {
  fuga: Fuga;

  constructor(fuga: Fuga) {
    this.fuga = fuga;
  }
}

const container = Container.getInstance();
const foo = container.resolve(Foo);
foo.fuga.hoge.call();
foo.fuga.piyo.call();
$ node dist/example/test2.js
hogeeeeeeeeeeee
piyooooooooooooo

まとめ

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


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

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

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

nvm チートシート

前書き

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

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

チートシート

前提

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

基本

ヘルプを表示する

$ nvm --help

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

$ nvm --version

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

$ nvm version <version>

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

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

$ nvm current

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

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

$ nvm install <version>

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

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

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

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

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

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

$ nvm install <version> --latest-npm

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

$ nvm uninstall <version>

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

$ nvm use [--silent] <version>

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

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

nvm ls [<version>]

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

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

nvm ls-remote [<version>]

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

nvm install-latest-npm

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

nvm reinstall-packages <version>

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

nvm which [current | <version>]

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

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

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

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

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

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

便利な使い方

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

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

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

参考リンク

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

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

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

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

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

はじめに

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

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

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

for-await-of loops

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

for-await-of_loops1.js
const fs = require('fs');

const reader = async (rs) => {
  for await (const chunk of rs) {
    console.log(chunk);
  }
};
(async function () {
  const rs = fs.createReadStream('./input.txt', { encoding: 'utf8' });
  await reader(rs);
})();
input.txt
abcde
fghij
klmno
pqrxy
z

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

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

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

for-await-of_loops2.js
const fs = require('fs');
const stream = require('stream');
const util = require('util');

const finished = util.promisify(stream.finished);
const msleep = util.promisify(setTimeout);

// streamを用いた場合の処理(全体終了部分のみのpromise化)
const reader1 = async (rs) => {
  rs.on('data', (chunk) => {
    console.log(chunk);
  });

  await finished(rs);
  // stream.finishedを使わない場合下記のようなpromiseを生成する
  // await new Promise((resolve, reject) => {
  //   rs.once('finished', (chunk) => {
  //     return resolve(data);
  //   });
  //   rs.once('error', (err) => {
  //     return reject(err);
  //   });
  // });
};

// streamを用いた場合の処理(chunk単位でのpromise化)
const reader2 = async (rs, iterator) => {
  let buffer = '';
  rs.on('data', (chunk) => {
    buffer = chunk;
    rs.pause();
  });

  let isEnd = false;
  rs.once('end', () => {
    isEnd = true;
  });
  let error;
  rs.once('error', (err) => {
    error = err;
  });

  while (true) {
    if (error) {
      throw error;
    }
    if (buffer) {
      await iterator(buffer);
      buffer = '';
    } else if (isEnd) {
      return;
    } else if (rs.isPaused()) {
      rs.resume();
    }
    // 非同期メソッドがないと無限ループしてしまうため
    await msleep(0);
  }
}

(async function () {
  const rs1 = fs.createReadStream('./input.txt', { encoding: 'utf8' });
  await reader1(rs1);
  const rs2 = fs.createReadStream('./input.txt', { encoding: 'utf8' });
  await reader2(rs2, async (chunk) => {
    console.log(chunk);
  });
})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z
abcde
fghij
klmno
pqrxy
z

async-generators

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

async-generators1.js
const util = require('util');
const msleep = util.promisify(setTimeout);

async function* generate() {
  for (let i = 1; i <= 3; i++) {
    await msleep(1000);
    yield i;
  }
}

const asyncIterator = generate();
(async () => {
  console.log(await asyncIterator.next());
  console.log(await asyncIterator.next());
  console.log(await asyncIterator.next());
  console.log(await asyncIterator.next());
})();

実行結果

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

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

async-generators2.js
const util = require('util');
const msleep = util.promisify(setTimeout);

async function* generate() {
  for (let i = 1; i <= 3; i++) {
    await msleep(1000);
    yield i;
  }
}

const asyncIterator = generate();
(async () => {
  for await (const v of asyncIterator) {
    console.log(v);
  }
})();

実行結果

1
2
3

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

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

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

line-reader1.js
const fs = require('fs');
const readline = require('readline');
const util = require('util');

const msleep = util.promisify(setTimeout);

const asyncLineReader = async (iterater) => {
  const rl = readline.createInterface({
    input: fs.createReadStream('input.txt', { encoding: 'utf8' }),
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    await iterater(line);
  }
}

(async () => {
  await asyncLineReader(async (line) => {
    await msleep(100);
    console.log(line);
  });
})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

解説

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

stream(for-await-of利用)

line-reader2.js
const fs = require('fs');
const util = require('util');

const msleep = util.promisify(setTimeout);

// streamを用いた場合の処理(for-await-of使用)
const asyncLineReader = async (iterater) => {
  const rs = fs.createReadStream('./input.txt', { encoding: 'utf8' });

  let buffer = '';
  for await (const chunk of rs) {
    buffer += chunk;
    const list = buffer.split('\n');
    // 最後の要素は改行が含まれているわけではないため、bufferに戻す
    buffer = list.pop();
    for (let i = 0; i < list.length; i++) {
      await iterater(list[i]);
    }
  }
  if (buffer) {
    // 終了時にbufferに残っている文字列もiteratorにわたす
    await iterater(buffer);
  }
}

(async () => {
  await asyncLineReader(async (line) => {
    await msleep(100);
    console.log(line);
  });
})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

解説

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

stream(for-await-of未使用)

line-reader2.js
const fs = require('fs');
const util = require('util');

const msleep = util.promisify(setTimeout);

// streamを用いた場合の処理(for-await-of未使用)
const asyncLineReader = async (iterator) => {
  const rs = fs.createReadStream('./input.txt', { encoding: 'utf8' });

  let buffer = '';
  let rows = [];
  rs.on('data', (chunk) => {
    buffer += chunk;
    const list = buffer.split('\n');
    buffer = list.pop();
    if (list.length) {
      rows.push(...list);
      rs.pause();
    }
  });

  let isEnd = false;
  rs.once('end', () => {
    isEnd = true;
  });
  let error;
  rs.once('error', (err) => {
    error = err;
  });

  while (true) {
    if (error) {
      // errorがあれば終了
      throw error;
    }

    if (rows.length) {
      for (let i = 0; i < rows.length; i++) {
        await iterator(rows[i]);
      }
      rows = [];
    } else if (isEnd) {
      if (buffer) {
        // 終了時にbufferに残っている文字列もiteratorにわたす
        await iterator(buffer);
      }
      return;
    } else if (rs.isPaused()) {
      rs.resume();
    }
    // 非同期メソッドがないと無限ループしてしまうため、setImmediate代わりに実行
    await msleep(0);
  }
}

(async () => {
  await asyncLineReader(async (line) => {
    await msleep(100);
    console.log(line);
  });
})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

解説

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

まとめ

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

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

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

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

1. 方法

前提

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

nodebrew と avn

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

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

手順

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

2. Trouble Shooting

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

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

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

terminal
v10.17.0

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

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

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

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

terminal
sudo rm -rf ~/.npm

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

terminal
node -v
npm -v

nodebrew を入れ直してみる

nodebrew を削除する

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

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

terminal
brew uninstall nodebrew

nodebrew を入れ直す

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

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

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

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

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

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

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

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

.bash_profile
source ~/.bashrc

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

avn could not activate node v6.17.1 と出る

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

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

terminal
nodebrew ls

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

terminal
nodebrew install-binary 6.17.1

参考

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

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

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

ごあいさつ

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

注意

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

この記事は......

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

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

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

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

そもそも何が難しいのか

バックエンドとは何か

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

DBについて

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

MongoDBのわかりにくさ

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

const mongodb = require('mongodb');
const MongoClient = mongodb.MongoClient;
const client = await MongoClient.connect('mongodb://127.0.0.1:27017/', {
            useUnifiedTopology: true,
            useNewUrlParser: true,
        });
const db = await client.db('dbName');
const collection = db.collection('collectionName');
const data = await collection.find().toArray();
client.close();

と書く必要があります。

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

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

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

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

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

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

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

使い方

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

mongodbeginnerのサンプル
const mob = require('mongodbeginner')
const data = await mob.find("dbName","collectionName",{id: 1})

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

mongodbeginnerのサンプル2
const mob = require('mongodbeginner')
const data = await mob.insert("dbName","collectionName",{id: 1, count: 0})

などとします。

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

問題点

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

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

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

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

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

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

まとめ

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

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

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

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

passportモジュールでfacebook認証

passportとは

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

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

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

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

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

Facebook Developersでアプリを作成

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

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

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

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

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

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

console
$ PORT=8000 npm start

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

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

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

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

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

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

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

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

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

app.js
var createError = require('http-errors');
//expressモジュールの読み込み
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

//passportモジュールの読み込み
var passport = require('passport');
//passport-facebookモジュールの読み込み
var Strategy = require('passport-facebook').Strategy;

//先ほど作成したアプリIDを変数に代入
var FACEBOOK_APP_ID = '44923726XXXXXX';
//先ほど作成したapp secretを変数に代入
var FACEBOOK_APP_SECRET = '170dde4751f2ee9d44140b8826457b63';

//passportモジュールによるシリアライズの設定
passport.serializeUser(function(user, done) {
  done(null, user);
});
//passportモジュールによるデシリアライズの設定
passport.deserializeUser(function(obj, done) {
  done(null, obj);
});

////passport-facebookモジュールのStarategy設定
passport.use(new Strategy({
  clientID: FACEBOOK_APP_ID,
  clientSecret: FACEBOOK_APP_SECRET,
  //facebook認証をするためのページの設定(固定)
  callbackURL: "http://localhost:8000/auth/facebook/callback",
  //プロフィールのどの情報を受け取ることができるかの設定
  profileFields: ['id', 'displayName']
},
  function (accessToken, refreshToken, profile, done) {
    //認証後にdone関数を返すために、process.nextTick関数を利用している
    process.nextTick(function () {
      return done(null, profile);
    });
  }
));

//index.jsの処理を利用するために変数に代入
var indexRouter = require('./routes/index');
//users.jsの処理を利用するための処理を変数に代入
var usersRouter = require('./routes/users');
//login.jsの処理を利用するための処理を変数に代入
var loginRouter = require('./routes/login');
//logout.jsの処理を利用するための処理を変数に代入
var logoutRouter = require('./routes/logout');

//expressモジュールを利用するためにapp変数に代入
var app = express();

//アプリケーションで利用するミドルウェアの設定
app.use(require('morgan')('combined'));
app.use(require('cookie-parser')());
app.use(require('body-parser').urlencoded({ extended: true }));
app.use(require('express-session')({ secret: 'keyboard cat', resave: true, saveUninitialized: true }));
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// viewsディレクトリの設定
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

//passportのの初期化
app.use(passport.initialize());
//ログイン後のセッション管理の設定
app.use(passport.session());

//localhost:8000/にアクセスした時にindexRouterの処理がなされる設定
app.use('/', indexRouter);
//localhost:8000/usersにアクセスした時にusersRouterの処理がなされる設定
app.use('/users', usersRouter);

//localhost:8000/auth/facebookにGETアクセスした時に認証リクエストを行う設定
app.get('/auth/facebook',
  passport.authenticate('facebook')
);
//localhost:8000/auth/facebook/callbackにGETアクセスした時に処理が行われる設定
app.get('/auth/facebook/callback',
 //処理が失敗した時のリダイレクト先の設定
  passport.authenticate('facebook', {failureRedirect: '/login' }),
  function(req, res) {
    //処理が成功した時のリダイレクト先の設定
    res.redirect('/');
  });

//localhost:8000/loginにアクセスした時にloginRouter処理がなされる設定
app.get('/login', loginRouter);

//localhost:8000/logoutにアクセスした時にlogoutRouter処理がなされる設定
app.get('/logout', logoutRouter);


//404エラーの処理設定
app.use(function(req, res, next) {
  next(createError(404));
});

// エラー処理の設定
app.use(function(err, req, res, next) {
  // ローカル環境のみ表示されるエラーの設定
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // エラーページを表示する設定
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

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

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

login.js
'use strict';
//expressの読み込み
var express = require('express');
//expressでルーターを使う設定
var router = express.Router();

//localhost:8000/loginにアクセスした際に、login.pugがレンダリングされる処理
router.get('/login', function(req, res) {
  res.render('login');
});

//モジュールのエキスポート
module.exports = router;
logout.js
'use strict';
//expressの読み込み
var express = require('express');
//expressでルーターを使う設定
var router = express.Router();

//localhost:8000/logoutにアクセスした際に、ログアウトされ、
//localhost:8000/にリダイレクトされる処理
router.get('/logout', function(req, res) {
  req.logout();
  res.redirect('/');
});

//モジュールのエキスポート
module.exports = router;
index.js
'use strict';
var express = require('express');
var router = express.Router();

//localhost:8000/にアクセスした際に、index.pugがレンダリングされ、
//index.pug内でtitleとuserが使えるようになる処理
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express', user: req.user });
});

module.exports = router;

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

index.pug
extends layout

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

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

login.pug
extends layout

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

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

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

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

console
PORT=8000 npm start

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

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

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

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

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

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

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

おまけ

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

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

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

app.js
app.use('/users', authenticatedUser, usersRouter);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

概要

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

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

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

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

アップグレード方法

$ nodenv install 12.13.0

$ nodenv local 12.13.0

エラー発生

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

$ yarn 

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

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

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

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

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

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

エラー対処

  • モジュールが使っているNodeに対応しているか確認
    スクリーンショット 2019-11-19 16.21.29.png
    https://github.com/sass/node-sass

  • Nodeに対応しているモジュールのインストール

 $ yarn add node-sass@4.12.0

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



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

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

エラー原因特定

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

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

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

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

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

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

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


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

解決

無事解決しました

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

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

参考サイト

GitHub Issues

resolutionsについて

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

Node.jsのCLIツールをカスタマイズ可能にする

Node.jsで何らかのCLIツールを作っているとします。

おそらくどのツールでも、オプショナルな機能を使うかどうかは、エンドユーザーに決めてもらいたいでしょう。その場合、-iとか-aのようにコマンドの引数をいくつか提供するのもいいですが、複雑な設定が必要になりそうだったら、設定ファイルでのカスタマイズを可能にした方がいいです。

設定ファイル

ファイル名

設定ファイルの名前は、ツールの開発者が決めないといけないのですが、ほとんどのプロジェクトでは、{tool}.config.jsという風に決まっています。たとえば、webpackだったら、webpack.config.jsというファイルで設定できます。

設定ファイルの検出

最近の多くのプロジェクトでは、設定を全てオプショナルにするトレンドがあります。もし設定ファイルが存在しなければ、デフォルトの値で動きます。設定ファイルが存在するかどうか、Node.jsのfsモジュールにあるexistsSyncという関数を使えばわかります。

existsSyncはダイレクトリーかファイルのパスを取るが、CLIツールのソースコードと、ユーザーのプロジェクトのルートはそれぞれ違う場所にあるので、ユーザーがCLIを呼んだダイレクトリーを検出しないといけないです。それはprocess.cwd()でできます。

const path = require('path');
const fs = require('fs');

const cwd = process.cwd();
const configPath = path.join(cwd, 'tool.config.json');

if (fs.existsSync(configPath)) {
  // 設定を読み込む
}

読み込み

これは設定ファイルの形式によりますが、多くのプロジェクトではJSONを使っています。JSONを読み込むには、fsにあるreadFileSyncでファイルを読み込み、その中身をJSON.parseします。

if (fs.existsSync(configPath)) {
  const rawConfig = fs.readFileSync(configPath);
  const config = JSON.parse(rawConfig);
}

fsの拡張であるfs-extraというライブラリには、fs.readJSONSyncという関数があるので、それを使うと一気にJSONのファイルが読み込めます。

もちろん、requireだけで読み込むこともできますし、jsonにもjsにも使えます。

if (fs.existsSync(configPath)) {
  const config = require(configPath);
}

ただし、一回requireされたものは、Node.jsプログラムのライフタイムに渡ってキャッシュされるので、CLIにはwatchのような機能を入れたい場合は、注意が必要です。設定ファイルが変更されても、たとえば2回目のビルドでキャッシュされた古いものがrequireされてしまいます。そういう問題を避けるためにfs-extraなどを使った方がいいです。

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

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

概要

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

Screen Shot 2019-12-02 at 17.30.38.png

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

対象者

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

背景

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

アンインストール方法

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

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

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

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

rm -f /usr/local/bin/node

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

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

rm -f /usr/local/bin/npm

さいごに

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

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

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

【Webpack4 備忘録】オススメ設定詰め合わせ

備忘録で作成したもの以外に入れた方がいいもののまとめ。

以前、備忘録で作成した こちら に設定を追加する。

目次

ソースマップにオリジナルのソースコードを表示する

build時にhtmlファイルも生成する

スタイルシートを要素として出力する

大きい画像ファイルを別ファイルとして出力する

ビルドのたびにbuildディレクトリをクリーンアップする

プロダクション環境へのデプロイモジュールを小さくする

ESLintとPrettierを導入する

ソースマップにオリジナルのソースコードを表示する

デフォルト設定のソースマップは以下の通り。

このままだと、デバッグが捗らない。

webpack-c-0

開発環境で、ソースマップにオリジナルのソースコードを表示する設定を追加するには、webpackにdevtoolの設定を追加する必要がある。

実装

webpack.conf.jsに以下の設定を追加する。

// 中略 ...

module.exports = {
  // 中略 ...
  // ソースマップの設定(オリジナルのソースコードを出力、コメントも出力される)
  devtool: 'eval-source-map',
  // 中略 ...
};

変更後は、以下の通りオリジナルのソースコードが表示されるようになる。

webpack-c-1

これで多少はデバッグがしやすくなるはず。

参考

本家のドキュメントのdevtoolのところ

build時にhtmlファイルも生成する

以前作成したボイラープレートのままだと、buildディレクトリにHTMLファイルは生成されない。

html-webpack-pluginを使用することで、ビルドコマンドを実行するたびにbuildディレクトリにHTMLファイルを生成できるようになる。

html-webpack-plugin とは

webpackで生成したJavaScriptやCSSを埋め込んだHTMLを生成できるプラグイン。

ディレクトリごとデプロイしたい場合などに使える。

実装

プラグインをインストールする。

$ npm install -D html-webpack-plugin

webpack.config.jshtml-webpack-pluginを読み込む。

const htmlWebpackPlugin = require('html-webpack-plugin');

// 中略 ...
module.exports = {
  // 中略 ...

  plugins: [
    // filenameで指定したJSを読み込むHTMLファイルをbuildディレクトリに生成
    new htmlWebpackPlugin()
  ],

// 中略 ...
}

// 中略 ...

ビルドコマンドを実行して確認する。

$ npm run build

> react-ts-webpack@1.0.0 build /Users/kento/Programing/VScodeProjects/ts-react-sass-simple-boiler-v4
> webpack

Hash: f56cebd02a6bb3a59a34
Version: webpack 4.38.0
Time: 2598ms
Built at: 2019-12-02 18:44:16
     Asset       Size  Chunks             Chunk Names
 bundle.js    967 KiB    main  [emitted]  main
index.html  182 bytes          [emitted]  
Entrypoint main = bundle.js
[./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/scss-style.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loade
r/src!./node_modules/sass-loader/lib/loader.js!./src/scss-style.scss 506 bytes {main} [built]
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built]
[./src/assets/img/react.png] 31.3 KiB {main} [built]
[./src/assets/img/vue.png] 5.48 KiB {main} [built]
[./src/index.tsx] 810 bytes {main} [built]
[./src/scss-style.scss] 1.35 KiB {main} [built]
    + 15 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {0} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

buildディレクトリにHTMLファイルが生成される。

$ tree ./build
./build
├── bundle.js
└── index.html

0 directories, 2 files

中身は、同階層のbundle.jsを読み込む設定で自動生成される。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="bundle.js"></script></body>
</html>

スタイルシートを要素として出力する

style-loadercss-loaderを使用することで、<style>という要素を埋め込むことができるが、スタイルシートは別ファイルとして<link>要素で埋め込むのが一般的である。

別ファイルとして読み込むためには、style-loader ではなく extract-text-webpack-pluginを使用する。

extract-text-webpack-pluginとは

バンドルしたリソースをファイルとして切り出すためのプラグイン。

実装

webpack用のプラグインをインストールする。

# latestバージョンでは、対応していないので @next をインストールする
$ npm install -D extract-text-webpack-plugin@next

webpack.config.jsにプラグインを追加する。

const extTextPlugin = require('extract-text-webpack-plugin');

// 中略 ...

const rules = [
  /* Sass用設定 */
  {
    // 対象とする拡張子を指定
    test: /\.scss$/,
    // extract-text-webpack-plugin に use で指定しているloaderを加える
    use: extTextPlugin.extract([
      // linkタグへの出力用 
      // 'style-loader',  extract-text-webpack-plugin を使うので不要
      // CSSのバンドル設定
      {
        loader: 'css-loader',
        options: {
          // css内のurl()メソッドを取り込む設定
          url: true,
          // // ソースマップの有効化 development と production で勝手に切り替わるのでコメントアウト
          // sourceMap: true,

          // sass-loader と postcss-loader を使用するので 2 を設定
          // ここを参考に設定 https://github.com/webpack-contrib/css-loader#importloaders
          importLoaders: 2,
        },
      },
      'postcss-loader',
      {
        loader: 'sass-loader',
        // options: {
        // // ソースマップの有効化 development と production で勝手に切り替わるのでコメントアウト
        //   sourceMap: true,
        // }
      },
    ]),
  },

  // 中略 ...

];

module.exports = {
  // 中略 ...

  plugins: [
    // 出力ファイル名を指定
    new extTextPlugin('[name].css'),
  ],

// 中略 ...
}

// 中略 ...


ビルドコマンドを実行して確認する。

$ npm run build

> react-ts-webpack@1.0.0 build /Users/kento/Programing/VScodeProjects/ts-react-sass-simple-boiler-v5
> webpack

Hash: f7c0f22ec54908cad1c7
Version: webpack 4.38.0
Time: 2071ms
Built at: 2019-12-02 19:12:25
     Asset       Size  Chunks             Chunk Names
 bundle.js   2.29 MiB    main  [emitted]  main
index.html  221 bytes          [emitted]  
  main.css   5.68 KiB    main  [emitted]  main
Entrypoint main = bundle.js main.css
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built]
[./src/assets/img/react.png] 31.3 KiB {main} [built]
[./src/assets/img/vue.png] 5.48 KiB [built]
[./src/index.tsx] 1.22 KiB {main} [built]
[./src/scss-style.scss] 41 bytes [built]
[./src/style1.scss] 41 bytes [built]
[./src/style2.scss] 41 bytes [built]
    + 14 hidden modules
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/scss-style.scss:
    Entrypoint undefined = extract-text-webpack-plugin-output-filename
    [./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/scss-style.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/scss-style.scss 506 bytes {0} [built]
    [./src/assets/img/vue.png] 5.48 KiB {0} [built]
        + 2 hidden modules
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/style1.scss:
    Entrypoint undefined = extract-text-webpack-plugin-output-filename
    [./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/style1.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/style1.scss 169 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/style2.scss:
    Entrypoint undefined = extract-text-webpack-plugin-output-filename
    [./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/style2.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/style2.scss 169 bytes {0} [built]
        + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {0} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

CSSファイルが別ファイルとして切り出されていることがわかる。

$ tree ./build
./build
├── bundle.js
├── index.html
└── main.css

0 directories, 3 files

./build/index.htmlにもLinkタグが追加されている。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  <link href="main.css" rel="stylesheet"></head>
  <body>
  <script type="text/javascript" src="bundle.js"></script></body>
</html>

CSSとJSを別ファイルとすることで、並行してのダウンロードができるので、パフォーマンス改善も期待できる。

大きい画像ファイルを別ファイルとして出力する

ファビコンなどの小さい画像イメージならJSにバンドルしてもパフォーマンスに影響しないが、数百KBの画像ファイルをJSにバンドルすると、パフォーマンスが悪くなる。

file-loaderを使う

file-loaderを使うことで、一定以上の大きさの画像ファイルはそのまま画像ファイルとして保持できる。

実装

file-loaderをインストールする。

$ npm install -D file-loader

web pack.config.jsに設定を追加する。

// 中略 ...

const rules = [
  // 中略 ...

  /* 画像ファイル用設定 */
  {
    // 対象となるファイルの拡張子を設定
    test: /\.(gif|png|jpg|jpeg|svg|ttf|eot|wof|woff|woff2)$/,
    // 画像をBase64で取り込み
    loader: 'url-loader',

    //// ↓ 追加 
    // 画像ファイルに関するオプション
    options: {
      // 20KB以上を対象とする(バイナリ換算)
      // https://www.gbmb.org/kb-to-bytes で正確な値を調べる
      limit: 20480,
      // ファイルのアウトプット場所 名前は保持
      name: "./images/[name].[ext]"
    }
  },
];

// 中略 ...

ビルドコマンドを実行して確認する。

$ npm run build

> react-ts-webpack@1.0.0 build /Users/kento/Programing/VScodeProjects/ts-react-sass-simple-boiler-v5
> webpack

Hash: c6425b45d4a5dcfdf726
Version: webpack 4.38.0
Time: 2676ms
Built at: 2019-12-02 19:36:26
             Asset       Size  Chunks             Chunk Names
./images/react.png   23.5 KiB          [emitted]  
         bundle.js   2.22 MiB    main  [emitted]  main
        index.html  221 bytes          [emitted]  
          main.css   5.68 KiB    main  [emitted]  main
Entrypoint main = bundle.js main.css
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built]
[./src/assets/img/react.png] 62 bytes {main} [built]
[./src/assets/img/vue.png] 5.48 KiB [built]
[./src/index.tsx] 1.22 KiB {main} [built]
[./src/scss-style.scss] 41 bytes [built]
[./src/style1.scss] 41 bytes [built]
[./src/style2.scss] 41 bytes [built]
    + 14 hidden modules
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/scss-style.scss:
    Entrypoint undefined = extract-text-webpack-plugin-output-filename
    [./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/scss-style.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/scss-style.scss 506 bytes {0} [built]
    [./src/assets/img/vue.png] 5.48 KiB {0} [built]
        + 2 hidden modules
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/style1.scss:
    Entrypoint undefined = extract-text-webpack-plugin-output-filename
    [./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/style1.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/style1.scss 169 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/style2.scss:
    Entrypoint undefined = extract-text-webpack-plugin-output-filename
    [./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/style2.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/style2.scss 169 bytes {0} [built]
        + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {0} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

vue.pngはないが20KB以上のreact.pngは存在する。 (react.pngは約25KBの画像ファイル)

$ tree ./build
./build
├── bundle.js
├── images
│   └── react.png
├── index.html
└── main.css

1 directory, 4 files

ビルドのたびにbuildディレクトリをクリーンアップする

clean-webpack-pluginを追加することで、ビルド先のディレクトリをビルド前にクリーンアップできる。(versionは3系を使用)

実装

clean-webpack-pluginをインストールする。

$ npm install -D clean-webpack-plugin

webpack.config.jsに設定を追加する。

outputオプションで指定したディレクトリがクリーンアップ対象となる。

// 中略 ...
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

// 中略 ...

module.exports = {
  // 中略 ...

  // 各種プラグイン
  plugins: [
    // ここでは、buildディレクトリをビルド前にクリーンアップ
    new CleanWebpackPlugin(),
  ],

  // 中略 ...
};

確認用にtext.txtを配置

$ $ tree ./build
./build
├── bundle.js
├── images
│   └── react.png
├── index.html
├── main.css
└── test.txt  ← ここ

1 directory, 5 files

ビルドコマンドを実行して確認する。

$ npm run build

> react-ts-webpack@1.0.0 build /Users/kento/Programing/VScodeProjects/ts-react-sass-simple-boiler-v5
> webpack

Hash: c6425b45d4a5dcfdf726
Version: webpack 4.38.0
Time: 2772ms
Built at: 2019-12-03 09:38:26
             Asset       Size  Chunks             Chunk Names
./images/react.png   23.5 KiB          [emitted]  
         bundle.js   2.22 MiB    main  [emitted]  main
        index.html  221 bytes          [emitted]  
          main.css   5.68 KiB    main  [emitted]  main
Entrypoint main = bundle.js main.css
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built]
[./src/assets/img/react.png] 62 bytes {main} [built]
[./src/assets/img/vue.png] 5.48 KiB [built]
[./src/index.tsx] 1.22 KiB {main} [built]
[./src/scss-style.scss] 41 bytes [built]
[./src/style1.scss] 41 bytes [built]
[./src/style2.scss] 41 bytes [built]
    + 14 hidden modules
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/scss-style.scss:
    Entrypoint undefined = extract-text-webpack-plugin-output-filename
    [./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/scss-style.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/scss-style.scss 506 bytes {0} [built]
    [./src/assets/img/vue.png] 5.48 KiB {0} [built]
        + 2 hidden modules
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/style1.scss:
    Entrypoint undefined = extract-text-webpack-plugin-output-filename
    [./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/style1.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/style1.scss 169 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/style2.scss:
    Entrypoint undefined = extract-text-webpack-plugin-output-filename
    [./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js!./node_modules/sass-loader/lib/loader.js!./src/style2.scss] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/style2.scss 169 bytes {0} [built]
        + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {0} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 2 hidden modules

buildディレクトリを削除して、再作成するためtest.txtは存在しない。

$ tree ./build
./build
├── bundle.js
├── images
│   └── react.png
├── index.html
└── main.css

1 directory, 4 files

プロダクション環境へのデプロイモジュールを小さくする

ビルドコマンドでproductionを指定している場合、ソースコードは最適化されるが、一部不要なコードやCSSの最適化までは行われない。

uglifyjs-webpack-pluginoptimize-css-assets-webpack-pluginで最適化できる。

uglifyjs-webpack-pluginとは

プロダクション環境で関数を除去できるプラグイン。

例えば、本番環境では、console.log()は必要ないから削除したいなど。

実装

uglifyjs-webpack-pluginをインストールする。

$ npm install -D uglifyjs-webpack-plugin

webpack.config.jsに設定を追加する。

// 中略 ...
const uglifyJsPlugin = require('uglifyjs-webpack-plugin');

// 中略 ...

module.exports = {
  // 中略 ...

  // プロダクション環境での設定(minimizer)
  optimization: {
    minimizer: [
      new uglifyJsPlugin({
        uglifyOptions: {
          compress: {
            // console.log() console.table()等除去
            drop_console: true
          }
        }
      })
    ]
  },

  // 中略 ...
};

optimize-css-assets-webpack-pluginとは

XXX.min.cssのように改行などを削除したCSSを出力できるプラグイン。

実装

optimize-css-assets-webpack-pluginをインストールする。

$ npm install -D optimize-css-assets-webpack-plugin

web pack.config.jsに設定を追加する。

// 中略 ...

// uglifyjs-webpack-pluginは必須
const uglifyJsPlugin = require('uglifyjs-webpack-plugin');
const optimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

// 中略 ...

module.exports = {
  // 中略 ...

  // プロダクション環境での設定(minimizer)
  optimization: {
    minimizer: [
      // 必須
      new uglifyJsPlugin({
        // 省略 ...
      }),
      // スタイルシートの圧縮設定
      new optimizeCssAssetsPlugin({}),
    ]
  },

  // 中略 ...
};

変更前

$ npm run build-prod

> react-ts-webpack@1.0.0 build-prod /Users/kento/Programing/VScodeProjects/ts-react-sass-simple-boiler-v5
> webpack --mode=production

Hash: f2a87911c7157885324e
Version: webpack 4.38.0
Time: 2916ms
Built at: 2019-12-03 10:22:04
             Asset       Size  Chunks                    Chunk Names
./images/react.png   23.5 KiB          [emitted]         
         bundle.js    336 KiB       0  [emitted]  [big]  main
        index.html  221 bytes          [emitted]         
          main.css   5.68 KiB       0  [emitted]         main  ← ここ


中略 ...

変更後

$ npm run build-prod

> react-ts-webpack@1.0.0 build-prod /Users/kento/Programing/VScodeProjects/ts-react-sass-sim
ple-boiler-v5
> webpack --mode=production

Hash: f2a87911c7157885324e
Version: webpack 4.38.0
Time: 2357ms
Built at: 2019-12-03 10:20:39
             Asset       Size  Chunks                    Chunk Names
./images/react.png   23.5 KiB          [emitted]         
         bundle.js    336 KiB       0  [emitted]  [big]  main
        index.html  221 bytes          [emitted]         
          main.css   5.63 KiB       0  [emitted]         main  ← ここ

中略 ...

変更後の方がCSSファイルのサイズが小さくなる。

ESLintとPrettierを導入する

構文解析を実装するため、ESLintとPrettierを使用する。

実装

exlintprettierとその他必要なプラグインをインストールする。

$ npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-plugin-prettier eslint-config-prettier

プロジェクトルートに.eslintrcを作成する。

{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "rules": {
    "prettier/prettier": [
      "error",
      {
        "singleQuote": true,
        "semi": true,
        "no-var-requires": true
      }
    ]
  }
}

package.jsonにスクリプトを追加する。

{
  // 中略 ...

  "scripts": {
    // 中略 ...

    "lint": "eslint --fix --ext .tsx,.ts ./src",
    "type-check": "tsc"

  },

  // 中略 ...
}


今回作成したものは こちら

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

Node.jsでOSを検出する方法

Node.js組み込みの関数が使えるのでとても簡単です。

process.platformでOS検出の結果がわかります。

// 例
const isWin = process.platform === "win32"
const isMacOS = process.platform === "darwin"

全ての可能な値

  • aix
  • darwin
  • freebsd
  • linux
  • openbsd
  • sunos
  • win32

注意

  • macOSはdarwinとして検出されます
  • Windowsは32-bitでも64-bitでもwin32になります
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nodebrew 本体が削除できなかった話

Nodebrew本体を削除する方法 に従い、Finder から削除したのに

terminal
$ nodebrew -v

でバージョンが表示され続け、消せていないようだった。

解決法

Homebrew で入れた nodebrew が残っているのが原因だったよう。

以下のコマンドを実行すると、 nodebrew -v でもバージョンが表示されなくなり、nodebrew の削除が完了した。

terminal
$ brew uninstall nodebrew

参考にした記事

brew uninstall nodebrew

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

jwtについて

YoutubeのfreeCodeCamp.orgでJWTについて動画があったのでまとめます。自分用に(笑。

jsonwebtokenの使い方について https://www.npmjs.com/package/jsonwebtoken。

この動画の中でserver側とfront側で分けていたのでそれに習います。

server側では
cors と cookie-parserを使ってログイン情報を受け取ります。
https://www.npmjs.com/package/corsから

Simple Usage (Enable All CORS Requests)
var express = require('express')
var cors = require('cors')
var app = express()

app.use(cors())

app.get('/products/:id', function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for all origins!'})
})

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

cookie-parser(https://www.npmjs.com/package/cookie-parser)

Example
var express = require('express')
var cookieParser = require('cookie-parser')

var app = express()
app.use(cookieParser())

app.get('/', function (req, res) {
  // Cookies that have not been signed
  console.log('Cookies: ', req.cookies)

  // Cookies that have been signed
  console.log('Signed Cookies: ', req.signedCookies)
})

app.listen(8080)

githubにアップされていたコードのコメントがわかりやすいのでそのまま上げます。

https://github.com/weibenfalk/jwtToken-react-express/blob/master/server/src/index.js

require('dotenv/config');
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const { verify } = require('jsonwebtoken');
const { hash, compare } = require('bcryptjs');
const {
  createAccessToken,
  createRefreshToken,
  sendRefreshToken,
  sendAccessToken,
} = require('./tokens.js');
const { fakeDB } = require('./fakeDB.js');
const { isAuth } = require('./isAuth.js');

// 1. Register a user
// 2. Login a user
// 3. Logout a user
// 4. Setup a protected route
// 5. Get a new accesstoken with a refresh token

const server = express();

// Use express middleware for easier cookie handling
server.use(cookieParser());

server.use(
  cors({
    origin: 'http://localhost:3000',
    credentials: true,
  }),
);

// Needed to be able to read body data
server.use(express.json()); // to support JSON-encoded bodies
server.use(express.urlencoded({ extended: true })); // to support URL-encoded bodies

// 1. Register a user
server.post('/register', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Check if the user exist
    const user = fakeDB.find(user => user.email === email);
    if (user) throw new Error('User already exist');
    // 2. If not user exist already, hash the password
    const hashedPassword = await hash(password, 10);
    // 3. Insert the user in "database"
    fakeDB.push({
      id: fakeDB.length,
      email,
      password: hashedPassword,
    });
    res.send({ message: 'User Created' });
    console.log(fakeDB);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 2. Login a user
server.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Find user in array. If not exist send error
    const user = fakeDB.find(user => user.email === email);
    if (!user) throw new Error('User does not exist');
    // 2. Compare crypted password and see if it checks out. Send error if not
    const valid = await compare(password, user.password);
    if (!valid) throw new Error('Password not correct');
    // 3. Create Refresh- and Accesstoken
    const accesstoken = createAccessToken(user.id);
    const refreshtoken = createRefreshToken(user.id);
    // 4. Store Refreshtoken with user in "db"
    // Could also use different version numbers instead.
    // Then just increase the version number on the revoke endpoint
    user.refreshtoken = refreshtoken;
    // 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
    sendRefreshToken(res, refreshtoken);
    sendAccessToken(res, req, accesstoken);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 3. Logout a user
server.post('/logout', (_req, res) => {
  res.clearCookie('refreshtoken', { path: '/refresh_token' });
  // Logic here for also remove refreshtoken from db
  return res.send({
    message: 'Logged out',
  });
});

// 4. Protected route
server.post('/protected', async (req, res) => {
  try {
    const userId = isAuth(req);
    if (userId !== null) {
      res.send({
        data: 'This is protected data.',
      });
    }
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 5. Get a new access token with a refresh token
server.post('/refresh_token', (req, res) => {
  const token = req.cookies.refreshtoken;
  // If we don't have a token in our request
  if (!token) return res.send({ accesstoken: '' });
  // We have a token, let's verify it!
  let payload = null;
  try {
    payload = verify(token, process.env.REFRESH_TOKEN_SECRET);
  } catch (err) {
    return res.send({ accesstoken: '' });
  }
  // token is valid, check if user exist
  const user = fakeDB.find(user => user.id === payload.userId);
  if (!user) return res.send({ accesstoken: '' });
  // user exist, check if refreshtoken exist on user
  if (user.refreshtoken !== token)
    return res.send({ accesstoken: '' });
  // token exist, create new Refresh- and accesstoken
  const accesstoken = createAccessToken(user.id);
  const refreshtoken = createRefreshToken(user.id);
  // update refreshtoken on user in db
  // Could have different versions instead!
  user.refreshtoken = refreshtoken;
  // All good to go, send new refreshtoken and accesstoken
  sendRefreshToken(res, refreshtoken);
  return res.send({ accesstoken });
});

server.listen(process.env.PORT, () =>
  console.log(`Server listening on port ${process.env.PORT}!`),
);

会員登録のメソッドについて

server.post('/register', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Check if the user exist
    const user = fakeDB.find(user => user.email === email);
    if (user) throw new Error('User already exist');
    // 2. If not user exist already, hash the password
    const hashedPassword = await hash(password, 10);
    // 3. Insert the user in "database"
    fakeDB.push({
      id: fakeDB.length,
      email,
      password: hashedPassword,
    });
    res.send({ message: 'User Created' });
    console.log(fakeDB);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

DBの中に会員データが見つかったらエラーを返し、見つからなかった場合DBにデータ挿入と板感じですね。

会員ログインのメソッドについて

// 2. Login a user
server.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Find user in array. If not exist send error
    const user = fakeDB.find(user => user.email === email);
    if (!user) throw new Error('User does not exist');
    // 2. Compare crypted password and see if it checks out. Send error if not
    const valid = await compare(password, user.password);
    if (!valid) throw new Error('Password not correct');
    // 3. Create Refresh- and Accesstoken
    const accesstoken = createAccessToken(user.id);
    const refreshtoken = createRefreshToken(user.id);
    // 4. Store Refreshtoken with user in "db"
    // Could also use different version numbers instead.
    // Then just increase the version number on the revoke endpoint
    user.refreshtoken = refreshtoken;
    // 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
    sendRefreshToken(res, refreshtoken);
    sendAccessToken(res, req, accesstoken);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

説明しなくても解ると思います。(笑
念の為このコードで使われているAccesstoken、Refreshtokenについては

https://auth0.com/blog/jp-refresh-tokens-what-are-they-and-when-to-use-them/ 
 に書かれている通り

Access Token はリソースに直接アクセスするために必要な情報を保持しています。つまり、クライアントがリソースを管理するサーバーにAccess Tokenをパスするとき、そのサーバーはそのトークンに含まれている情報を使用してクライアントが認可したものかを判断します。Access Tokenには通常、有効期限があり、存続期間は短いです。
Refresh Token は新しいAccess Tokenを取得するために必要な情報を保持しています。つまり、特定リソースにアクセスする際に、Access Tokenが必要な場合には、クライアントはAuthorization Serverが発行する新しいAccess Tokenを取得するためにRefresh Tokenを使用します。一般的な使用方法は、Access Tokenの期限が切れた後に新しいものを取得したり、初めて新しいリソースにアクセスするときなどです。Refresh Tokenにも有効期限がありますが、存続期間はAccess Tokenよりも長くなっています。Refresh Tokenは通常、漏洩しないように厳しいストレージ要件が課せられます。Authorization Serverによってブラックリストに載ることもあります。

src/isAuth.jsについて

const { verify } = require('jsonwebtoken');

const isAuth = req => {
  const authorization = req.headers['authorization'];
  if (!authorization) throw new Error('You need to login.');
  // Based on 'Bearer ksfljrewori384328289398432'
  const token = authorization.split(' ')[1];
  const { userId } = verify(token, process.env.ACCESS_TOKEN_SECRET);
  return userId;
};

module.exports = {
  isAuth,
};

jsのsplit文についてはhttps://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split

var str = 'The quick brown fox jumps over the lazy dog.';

var words = str.split(' ');
console.log(words[3]);
// expected output: "fox"

var chars = str.split('');
console.log(chars[8]);
// expected output: "k"

var strCopy = str.split();
console.log(strCopy);
// expected output: Array ["The quick brown fox jumps over the lazy dog."]

front側については
src/App.js

import React, { useState, useEffect } from 'react';
import { Router, navigate } from '@reach/router';

import Navigation from './components/Navigation';
import Login from './components/Login';
import Register from './components/Register';
import Protected from './components/Protected';
import Content from './components/Content';

export const UserContext = React.createContext([]);

function App() {
  const [user, setUser] = useState({});
  const [loading, setLoading] = useState(true);

  const logOutCallback = async () => {
    await fetch('http://localhost:4000/logout', {
      method: 'POST',
      credentials: 'include', // Needed to include the cookie
    });
    // Clear user from context
    setUser({});
    // Navigate back to startpage
    navigate('/');
  }

  // First thing, check if a refreshtoken exist
  useEffect(() => {
    async function checkRefreshToken() {
      const result = await (await fetch('http://localhost:4000/refresh_token', {
        method: 'POST',
        credentials: 'include', // Needed to include the cookie
        headers: {
          'Content-Type': 'application/json',
        }
      })).json();
        setUser({
          accesstoken: result.accesstoken,
        });
        setLoading(false);
    }
    checkRefreshToken();
  }, []);

  if (loading) return <div>Loading ...</div>

  return (
    <UserContext.Provider value={[user, setUser]}>
      <div className="app">
        <Navigation logOutCallback={logOutCallback} />
        <Router id="router">
          <Login path="login" />
          <Register path="register" />
          <Protected path="protected" />
          <Content path="/" />
        </Router>
      </div>
    </UserContext.Provider>
  );
}

export default App;

src/components/Register.jsについては

import React, { useState } from 'react';
import { navigate } from '@reach/router';

const Register = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async e => {
    e.preventDefault();
    const result = await (await fetch('http://localhost:4000/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: email,
        password: password,
      }),
    })).json();
    if (!result.error) {
      console.log(result.message);
      navigate('/');
    } else {
      console.log(result.error);
    }
  };

  const handleChange = e => {
    if (e.currentTarget.name === 'email') {
      setEmail(e.currentTarget.value);
    } else {
      setPassword(e.currentTarget.value);
    }
  };

  return (
    <div className="login-wrapper">
      <form onSubmit={handleSubmit}>
        <div>Register</div>
        <div className="login-input">
          <input
            value={email}
            onChange={handleChange}
            type="text"
            name="email"
            placeholder="Email"
            autoComplete="email"
          />
          <input
            value={password}
            onChange={handleChange}
            type="password"
            name="password"
            autoComplete="current-password"
            placeholder="Password"
          />
          <button type="submit">Register</button>
        </div>
      </form>
    </div>
  );
};

export default Register;

front側についてはそのままありのままなので詳しくは説明できません。
見てくれた方には心苦しいですが、赦してほしいです。
では、また。

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

JWTについて

YoutubeのfreeCodeCamp.orgでJWTについて動画があったのでまとめます。自分用に(笑。

jsonwebtokenの使い方について https://www.npmjs.com/package/jsonwebtoken

この動画の中でserver側とfront側で分けていたのでそれに習います。

server側では
cors と cookie-parserを使ってログイン情報を受け取ります。
https://www.npmjs.com/package/corsから

Simple Usage (Enable All CORS Requests)
var express = require('express')
var cors = require('cors')
var app = express()

app.use(cors())

app.get('/products/:id', function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for all origins!'})
})

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

cookie-parser(https://www.npmjs.com/package/cookie-parser)

Example
var express = require('express')
var cookieParser = require('cookie-parser')

var app = express()
app.use(cookieParser())

app.get('/', function (req, res) {
  // Cookies that have not been signed
  console.log('Cookies: ', req.cookies)

  // Cookies that have been signed
  console.log('Signed Cookies: ', req.signedCookies)
})

app.listen(8080)

githubにアップされていたコードのコメントがわかりやすいのでそのまま上げます。

https://github.com/weibenfalk/jwtToken-react-express/blob/master/server/src/index.js

require('dotenv/config');
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const { verify } = require('jsonwebtoken');
const { hash, compare } = require('bcryptjs');
const {
  createAccessToken,
  createRefreshToken,
  sendRefreshToken,
  sendAccessToken,
} = require('./tokens.js');
const { fakeDB } = require('./fakeDB.js');
const { isAuth } = require('./isAuth.js');

// 1. Register a user
// 2. Login a user
// 3. Logout a user
// 4. Setup a protected route
// 5. Get a new accesstoken with a refresh token

const server = express();

// Use express middleware for easier cookie handling
server.use(cookieParser());

server.use(
  cors({
    origin: 'http://localhost:3000',
    credentials: true,
  }),
);

// Needed to be able to read body data
server.use(express.json()); // to support JSON-encoded bodies
server.use(express.urlencoded({ extended: true })); // to support URL-encoded bodies

// 1. Register a user
server.post('/register', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Check if the user exist
    const user = fakeDB.find(user => user.email === email);
    if (user) throw new Error('User already exist');
    // 2. If not user exist already, hash the password
    const hashedPassword = await hash(password, 10);
    // 3. Insert the user in "database"
    fakeDB.push({
      id: fakeDB.length,
      email,
      password: hashedPassword,
    });
    res.send({ message: 'User Created' });
    console.log(fakeDB);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 2. Login a user
server.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Find user in array. If not exist send error
    const user = fakeDB.find(user => user.email === email);
    if (!user) throw new Error('User does not exist');
    // 2. Compare crypted password and see if it checks out. Send error if not
    const valid = await compare(password, user.password);
    if (!valid) throw new Error('Password not correct');
    // 3. Create Refresh- and Accesstoken
    const accesstoken = createAccessToken(user.id);
    const refreshtoken = createRefreshToken(user.id);
    // 4. Store Refreshtoken with user in "db"
    // Could also use different version numbers instead.
    // Then just increase the version number on the revoke endpoint
    user.refreshtoken = refreshtoken;
    // 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
    sendRefreshToken(res, refreshtoken);
    sendAccessToken(res, req, accesstoken);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 3. Logout a user
server.post('/logout', (_req, res) => {
  res.clearCookie('refreshtoken', { path: '/refresh_token' });
  // Logic here for also remove refreshtoken from db
  return res.send({
    message: 'Logged out',
  });
});

// 4. Protected route
server.post('/protected', async (req, res) => {
  try {
    const userId = isAuth(req);
    if (userId !== null) {
      res.send({
        data: 'This is protected data.',
      });
    }
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 5. Get a new access token with a refresh token
server.post('/refresh_token', (req, res) => {
  const token = req.cookies.refreshtoken;
  // If we don't have a token in our request
  if (!token) return res.send({ accesstoken: '' });
  // We have a token, let's verify it!
  let payload = null;
  try {
    payload = verify(token, process.env.REFRESH_TOKEN_SECRET);
  } catch (err) {
    return res.send({ accesstoken: '' });
  }
  // token is valid, check if user exist
  const user = fakeDB.find(user => user.id === payload.userId);
  if (!user) return res.send({ accesstoken: '' });
  // user exist, check if refreshtoken exist on user
  if (user.refreshtoken !== token)
    return res.send({ accesstoken: '' });
  // token exist, create new Refresh- and accesstoken
  const accesstoken = createAccessToken(user.id);
  const refreshtoken = createRefreshToken(user.id);
  // update refreshtoken on user in db
  // Could have different versions instead!
  user.refreshtoken = refreshtoken;
  // All good to go, send new refreshtoken and accesstoken
  sendRefreshToken(res, refreshtoken);
  return res.send({ accesstoken });
});

server.listen(process.env.PORT, () =>
  console.log(`Server listening on port ${process.env.PORT}!`),
);

会員登録のメソッドについて

server.post('/register', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Check if the user exist
    const user = fakeDB.find(user => user.email === email);
    if (user) throw new Error('User already exist');
    // 2. If not user exist already, hash the password
    const hashedPassword = await hash(password, 10);
    // 3. Insert the user in "database"
    fakeDB.push({
      id: fakeDB.length,
      email,
      password: hashedPassword,
    });
    res.send({ message: 'User Created' });
    console.log(fakeDB);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

DBの中に会員データが見つかったらエラーを返し、見つからなかった場合DBにデータ挿入と板感じですね。

会員ログインのメソッドについて

// 2. Login a user
server.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Find user in array. If not exist send error
    const user = fakeDB.find(user => user.email === email);
    if (!user) throw new Error('User does not exist');
    // 2. Compare crypted password and see if it checks out. Send error if not
    const valid = await compare(password, user.password);
    if (!valid) throw new Error('Password not correct');
    // 3. Create Refresh- and Accesstoken
    const accesstoken = createAccessToken(user.id);
    const refreshtoken = createRefreshToken(user.id);
    // 4. Store Refreshtoken with user in "db"
    // Could also use different version numbers instead.
    // Then just increase the version number on the revoke endpoint
    user.refreshtoken = refreshtoken;
    // 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
    sendRefreshToken(res, refreshtoken);
    sendAccessToken(res, req, accesstoken);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

説明しなくても解ると思います。(笑
念の為このコードで使われているAccesstoken、Refreshtokenについては
https://auth0.com/blog/jp-refresh-tokens-what-are-they-and-when-to-use-them/
に書かれている通り

Access Token はリソースに直接アクセスするために必要な情報を保持しています。つまり、クライアントがリソースを管理するサーバーにAccess Tokenをパスするとき、そのサーバーはそのトークンに含まれている情報を使用してクライアントが認可したものかを判断します。Access Tokenには通常、有効期限があり、存続期間は短いです。
Access Token

Refresh Token は新しいAccess Tokenを取得するために必要な情報を保持しています。つまり、特定リソースにアクセスする際に、Access Tokenが必要な場合には、クライアントはAuthorization Serverが発行する新しいAccess Tokenを取得するためにRefresh Tokenを使用します。一般的な使用方法は、Access Tokenの期限が切れた後に新しいものを取得したり、初めて新しいリソースにアクセスするときなどです。Refresh Tokenにも有効期限がありますが、存続期間はAccess Tokenよりも長くなっています。Refresh Tokenは通常、漏洩しないように厳しいストレージ要件が課せられます。Authorization Serverによってブラックリストに載ることもあります。

src/isAuth.jsについて

const { verify } = require('jsonwebtoken');

const isAuth = req => {
  const authorization = req.headers['authorization'];
  if (!authorization) throw new Error('You need to login.');
  // Based on 'Bearer ksfljrewori384328289398432'
  const token = authorization.split(' ')[1];
  const { userId } = verify(token, process.env.ACCESS_TOKEN_SECRET);
  return userId;
};

module.exports = {
  isAuth,
};

jsのsplit文についてはhttps://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split

var str = 'The quick brown fox jumps over the lazy dog.';

var words = str.split(' ');
console.log(words[3]);
// expected output: "fox"

var chars = str.split('');
console.log(chars[8]);
// expected output: "k"

var strCopy = str.split();
console.log(strCopy);
// expected output: Array ["The quick brown fox jumps over the lazy dog."]

front側については
src/App.js

import React, { useState, useEffect } from 'react';
import { Router, navigate } from '@reach/router';

import Navigation from './components/Navigation';
import Login from './components/Login';
import Register from './components/Register';
import Protected from './components/Protected';
import Content from './components/Content';

export const UserContext = React.createContext([]);

function App() {
  const [user, setUser] = useState({});
  const [loading, setLoading] = useState(true);

  const logOutCallback = async () => {
    await fetch('http://localhost:4000/logout', {
      method: 'POST',
      credentials: 'include', // Needed to include the cookie
    });
    // Clear user from context
    setUser({});
    // Navigate back to startpage
    navigate('/');
  }

  // First thing, check if a refreshtoken exist
  useEffect(() => {
    async function checkRefreshToken() {
      const result = await (await fetch('http://localhost:4000/refresh_token', {
        method: 'POST',
        credentials: 'include', // Needed to include the cookie
        headers: {
          'Content-Type': 'application/json',
        }
      })).json();
        setUser({
          accesstoken: result.accesstoken,
        });
        setLoading(false);
    }
    checkRefreshToken();
  }, []);

  if (loading) return <div>Loading ...</div>

  return (
    <UserContext.Provider value={[user, setUser]}>
      <div className="app">
        <Navigation logOutCallback={logOutCallback} />
        <Router id="router">
          <Login path="login" />
          <Register path="register" />
          <Protected path="protected" />
          <Content path="/" />
        </Router>
      </div>
    </UserContext.Provider>
  );
}

export default App;

src/components/Register.jsについては

import React, { useState } from 'react';
import { navigate } from '@reach/router';

const Register = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async e => {
    e.preventDefault();
    const result = await (await fetch('http://localhost:4000/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: email,
        password: password,
      }),
    })).json();
    if (!result.error) {
      console.log(result.message);
      navigate('/');
    } else {
      console.log(result.error);
    }
  };

  const handleChange = e => {
    if (e.currentTarget.name === 'email') {
      setEmail(e.currentTarget.value);
    } else {
      setPassword(e.currentTarget.value);
    }
  };

  return (
    <div className="login-wrapper">
      <form onSubmit={handleSubmit}>
        <div>Register</div>
        <div className="login-input">
          <input
            value={email}
            onChange={handleChange}
            type="text"
            name="email"
            placeholder="Email"
            autoComplete="email"
          />
          <input
            value={password}
            onChange={handleChange}
            type="password"
            name="password"
            autoComplete="current-password"
            placeholder="Password"
          />
          <button type="submit">Register</button>
        </div>
      </form>
    </div>
  );
};

export default Register;

front側についてはそのままありのままなので詳しくは説明できません。
見てくれた方には心苦しいですが、赦してほしいです。
では、また。

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

typescriptで書かれたnodeアプリ、クライアント サーバ どっちでコンパイルする?

クライアント側でコンパイルして、gitにコンパイル済みjsを含むよ派

  • メリット
    • サーバはpullして実行するだけだから余計なものが入らない
  • デメリット
    • gitにコンパイル済みファイルが混ざる。
    • ローカルでコンパイルするのを忘れて、コンパイル済みファイルがソースコードと違う場合が発生する

サーバ側でコンパイルするよ派

  • メリット
    • gitにはソースコードのみ存在して余計なものがない
  • デメリット
    • サーバにビルド環境を整えたくない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Paiza Cloud で LINE Bot を試してみる

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

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

Paiza Cloud とは

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

2019-12-03_01h00_54.png

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

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

image.png

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

サーバーを立ち上げる

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

image.png

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

image.png

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

image.png

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

LINE Bot をつくる

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

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

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

image.png

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

npm i @line/bot-sdk express

を実行します。

image.png

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

image.png

新規ファイルを押して、

image.png

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

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

server.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;

const config = {
    channelSecret: '作成したBOTのチャンネルシークレット',
    channelAccessToken: '作成したBOTのチャンネルアクセストークン'
};

const app = express();

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);

    //ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。
    if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
        res.send('Hello LINE BOT!(POST)');
        console.log('疎通確認用');
        return; 
    }

    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: event.message.text //実際に返信の言葉を入れる箇所
  });
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);

image.png

保存します。

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

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

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

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

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

const config = {
    channelSecret: 'channelSecret',
    channelAccessToken: 'channelAccessToken'
};

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

const config = {
    channelSecret: 'ABCDEFGHIJ',
    channelAccessToken: '1234567890'
};

と、なります。

ファイルを保存します。

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

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

node server.js

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

image.png

公開URL確認

image.png

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

image.png

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

image.png

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

これで準備完了です。

動かしてみる

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

image.png

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

image.png

ターミナルを見てみると

image.png

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

明日の記事は…

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

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

LINEログインを、Node.js + Herokuでやってみた

お仕事でLINEログインをチームで実装する機会があったので、自分でもできるように小さなサービスを作ってみようと思います。

まずは、LINEログインボタンを表示し、それを押すとLINEログインの実行を行い、ログインしたアカウント情報を表示するところまで実装します。

サーバサイドはNode.jsを利用し、インフラとしてはHerokuを利用します。
(LINEからのcallback URLを指定する必要があるので、Herokuが便利です。)

事前準備

Node: v12.13.1
Heroku CLI: 7.35.0

Nodeアプリ環境の構築

https://github.com/noboru-i/line-note/commit/d0d935c6075b5f3f007b306d7aeaf14b2134355e
https://github.com/noboru-i/line-note/commit/94518c502c16b300e63bc8d4bdf73eb95d9b3d9d

まずは、サンプルを持ってきて、フォルダ名などをリネーム。

git clone https://github.com/heroku/node-js-getting-started.git
mv node-js-getting-started line-note
cd line-note
npm install

その後、READMEやpackage.jsonのアプリ名部分などを整理。

Heroku環境の作成・仮デプロイ

heroku create line-note
git push heroku master
heroku open

これで、とりあえずサンプルのNode.jsアプリが動作していることを確認できた。

LINE Developersにて、LINEログインチャンネルを作成

https://developers.line.biz/console/ より、個人のLINEアカウントでログイン。

適当な名前を入れてProviderを作成。

いろいろ適当に入力し、LINEログインチャンネルを作成。

"LINE Login"タブの"Callback URL"に、 https://line-note.herokuapp.com/callback を指定。

Herokuアプリの環境変数として、チャンネルID / チャンネルシークレットを保存しておく。

heroku config:set LINECORP_PLATFORM_CHANNEL_CHANNELID=(コンソールで確認できる"Channel ID")
heroku config:set LINECORP_PLATFORM_CHANNEL_CHANNELSECRET=(コンソールで確認できる"Channel secret")

ローカルでの確認用に、 .env にも同様に設定しておく。
(とはいえ、callbackが本番ドメインの方に来るので、ローカル環境は使いませんでした。)

LINECORP_PLATFORM_CHANNEL_CHANNELID=(コンソールで確認できる"Channel ID")
LINECORP_PLATFORM_CHANNEL_CHANNELSECRET=(コンソールで確認できる"Channel secret")

コードの修正し、LINEログインを実装

https://github.com/noboru-i/line-note/commit/11af00edf4399acbc0452ba316d3a094031fd989

とりあえずログインボタンを作れればいいので、 views/pages/index.ejs を開いて、上の方にあるボタンを以下のように書き換える。

<a type="button" class="btn btn-lg btn-default" href="/login">LINEログイン</a>

次に、 /login にアクセスされたときに、LINEログインのページにリダイレクトする仕組みを作っておく。
同様に、 /callback にアクセスが来たとき、リクエストパラメータの code を出力することにしておく。
index.js を、以下のように変更。

const express = require('express')
const path = require('path')
const PORT = process.env.PORT || 5000

const querystring = require('querystring');

express()
  .use(express.static(path.join(__dirname, 'public')))
  .disable('etag')
  .set('views', path.join(__dirname, 'views'))
  .set('view engine', 'ejs')
  .get('/', (req, res) => res.render('pages/index'))
  .get('/login', (req, res) => {
    const query = querystring.stringify({
      response_type: 'code',
      client_id: process.env.LINECORP_PLATFORM_CHANNEL_CHANNELID,
      redirect_uri: 'https://line-note.herokuapp.com/callback',
      state: 'hoge', // TODO: must generate random string
      scope: 'profile',
    })
    res.redirect(301, 'https://access.line.me/oauth2/v2.1/authorize?' + query)
  })
  .get('/callback', (req, res) => {
    res.send('code: ' + req.query.code)
  })
  .listen(PORT, () => console.log(`Listening on ${ PORT }`))

これを git push heroku master にてデプロイすると、ログインのフローが確認できる。
まだ、codeが見えるだけ。

LINEの情報を取得する

https://github.com/noboru-i/line-note/commit/85ba05cae417ea5b004ff60fac1dc159c97f13b6
https://github.com/noboru-i/line-note/commit/d6e3f23e4a533147132c4ee613e57de746085d39

codeからaccess tokenを取得、それを利用してユーザ情報を取得する。

まずは、サーバからAPIを実行するために、requestモジュールを導入。

npm install request --save

そして、以下のように実装する。

const request = require('request')

express()
  // ...
  .get('/callback', (req, res) => {
    request
      .post({
        url: `https://api.line.me/oauth2/v2.1/token`,
        form: {
          grant_type: "authorization_code",
          code: req.query.code,
          redirect_uri: 'https://line-note.herokuapp.com/callback',
          client_id: process.env.LINECORP_PLATFORM_CHANNEL_CHANNELID,
          client_secret: process.env.LINECORP_PLATFORM_CHANNEL_CHANNELSECRET,
        }
      }, (error, response, body) => {
        if (response.statusCode != 200) {
          res.send(error)
          return
        }
        request
          .get({
            url: 'https://api.line.me/v2/profile',
            headers: {
              'Authorization': 'Bearer ' + JSON.parse(body).access_token
            }
          }, (error, response, body) => {
            if (response.statusCode != 200) {
              res.send(error)
              return
            }
            res.send(body)
          })
      })
  })
  .listen(PORT, () => console.log(`Listening on ${ PORT }`))

これによって、以下のようなレスポンスが画面に表示される。

{
    userId: "XXX",
    displayName: "XXX",
    pictureUrl: "https://profile.line-scdn.net/ch/v2/p/XXX"
}

今回やらなかったこと

access_token / refresh_token

access_token は30日で有効期限切れとなります。
access_token が失効したあと、10日以内に refresh_token を利用すると、再度 access_tokenrefresh_token を取得できます。

stateのチェック

CSRF対策のため、 /authorize にアクセスする際のstateパラメータにランダムな文字列を生成して、 /callback で返却されたstateとチェックする必要があります。
セッションなどに一時的に保存して、チェックする必要があります。

参考

https://developers.line.biz/ja/docs/line-login/web/try-line-login/
line-login-starterというサンプルアプリの使い方が書いてある。

https://github.com/nkjm/line-login
LINEログインのunofficial SDK。今回は参考として。

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

CSVデータから大量にHTMLファイルを生成する。

概要

神戸市が提供しているオープンデータ「観光施設情報」のCSVを元にHTMLを量産します。

制作物

・観光地一覧ページ(index.html)

index.jpg

・観光地詳細ページ(place**.html)

detail.jpg

開発環境

node v9.3.0
npm v6.12.1
gulp v4.0.2

package.json

{
  "name": "csv2html",
  "version": "0.0.1",
  "scripts": {
    "gulp": "gulp"
  },
  "devDependencies": {
    "autoprefixer": "^9.6.1",
    "csv-parser": "^2.3.2",
    "fs": "0.0.1-security",
    "gulp": "^4.0.2",
    "gulp-ejs": "^4.1.2",
    "gulp-postcss": "^8.0.0",
    "gulp-rename": "^1.4.0",
    "gulp-stylus": "^2.7.0"
  },
  "browserslist": [
    "last 2 versions",
    "ie >= 10",
    "Android >= 4"
  ]
}

読み込むCSV

名称,名称のルビ,郵便番号,カテゴリ,住所,緯度,経度,電話番号,説明文,営業時間,定休日,URL,バリアフリー対応

観光施設情報 CSVデータ

ソース

gulpfile.js

const gulp = require('gulp');
const postcss = require("gulp-postcss");
const autoprefixer = require('autoprefixer');
const stylus = require('gulp-stylus');
const rename = require('gulp-rename');
const ejs = require("gulp-ejs");
const fs = require('fs');
const csv = require('csv-parser');

/*-------------------------------------------------
--------------------------------------------------*/
gulp.task('stylus', function() {
    return gulp.src(['resources/stylus/**/*.styl','!resources/stylus/**/_*.styl'])
        .pipe(stylus({
            compress: true
        }))
        .pipe(postcss([
            autoprefixer({
                cascade: false
            })
        ]))
        .pipe(gulp.dest('htdocs/assets/css/'));
});

/*-------------------------------------------------
--------------------------------------------------*/
gulp.task( "ejs", function () {
    let json;
    const results = [];

    return fs.createReadStream(__dirname + '/resources/tourism_od2810.csv')
        .pipe(csv())
        .on('data', function(data){
            let zip = data['郵便番号'];
            data['郵便番号'] = zip.substr(1,3) + '-' + zip.substr(4,4);

            let cat = data['カテゴリ'];
            data['カテゴリ'] = cat.substr(1,4);
            results.push(data);
        })
        .on('end', function(){
            json = {
                item : results
            };
            gulp.src(["./resources/ejs/index.ejs"])
                .pipe(ejs(json))
                .pipe(rename(
                    {
                        extname: '.html'
                    }))
                .pipe( gulp.dest( "./htdocs" ) );

            ////////////////////////
            for(let prop in results){
                json = {
                    item : results[prop]
                };
                gulp.src(["./resources/ejs/place.ejs"])
                .pipe(ejs(json))
                .pipe(rename(
                    { 
                        basename: 'place' + prop, 
                        extname: '.html' 
                    }))
                .pipe( gulp.dest( "./htdocs" ) );

            }
        });
});
/*-------------------------------------------------
--------------------------------------------------*/
gulp.task('watch', function(){
    gulp.watch( 'resources/sass/**/*.scss', gulp.task('scss'));
    gulp.watch( 'resources/stylus/**/*.styl', gulp.task('stylus'));
    gulp.watch( 'resources/ejs/**/*.ejs', gulp.task('ejs'));
});

観光地一覧 index.ejs

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>神戸市観光施設情報</title>
    <link href="https://fonts.googleapis.com/css?family=Noto+Sans+JP&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="assets/css/style.css">
    <script src="assets/js/script.min.js"></script>
</head>
<body>
<header>
    <div class="header-inr">
        <a class="header-logo" href="index.html">
            <h1>神戸市観光施設情報</h1>
        </a>
    </div>
</header>

<div id="contents">
    <h2>神戸市観光施設 一覧</h2>
    <table>
        <% for (var prop in item) { %>
        <tr>
            <td><a href="place<%- prop %>.html"><%- item[prop]['名称'] %></a></td>
        </tr>
        <% } %>
    </table>

</div>

<footer>
Copyright 2019
</footer>
</body>
</html>

観光地詳細 place.ejs

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>神戸市観光施設情報</title>
    <link href="https://fonts.googleapis.com/css?family=Noto+Sans+JP&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="assets/css/style.css">
    <script src="assets/js/script.min.js"></script>
</head>
<body>
<header>
    <div class="header-inr">
        <a class="header-logo" href="index.html">
            <h1>神戸市観光施設情報</h1>
        </a>
    </div>
</header>
<ul>
    <li><a href="index.html">HOME</a></li>
    <li><%- item['名称'] %></li>
</ul>

<div id="contents">

        <h2><%- item['名称'] %></h2>
        <div><%- item['名称のルビ'] %></div>

         
        <iframe src="http://maps.google.co.jp/maps?q=<%- item['緯度'] %>,<%- item['経度'] %>&output=embed&t=m&z=20&hl=ja" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" width="100%" height="450"></iframe>
         
        <table>
            <tr>
                <th>郵便番号</th>
                <td><%- item['郵便番号'] %></td>
            </tr>
            <tr>
                <th>カテゴリ</th>
                <td><%- item['カテゴリ'] %></td>
            </tr>
            <tr>
                <th>住所</th>
                <td><%- item['住所'] %></td>
            </tr>
            <tr>
                <th>緯度</th>
                <td><%- item['緯度'] %></td>
            </tr>
            <tr>
                <th>経度</th>
                <td><%- item['経度'] %></td>
            </tr>
            <tr>
                <th>電話番号</th>
                <td><%- item['電話番号'] %></td>
            </tr>
            <tr>
                <th>説明文</th>
                <td><%- item['説明文'] %></td>
            </tr>
            <tr>
                <th>営業時間</th>
                <td><%- item['営業時間'] %></td>
            </tr>
            <tr>
                <th>定休日</th>
                <td><%- item['定休日'] %></td>
            </tr>
            <tr>
                <th>URL</th>
                <td><a href="<%- item['URL'] %>" target="_blank"><%- item['URL'] %></a></td>
            </tr>
            <tr>
                <th>バリアフリー対応</th>
                <td><%- item['バリアフリー対応'] %></td>
            </tr>
        </table>

</div>

<footer>
Copyright 2019
</footer>
</body>
</html>

Stylus

body
    margin 0
    padding 0
    background-color #325e91
    font-family 'Noto Sans JP', sans-serif
    color #333333

header
    padding 15px
    background #ffffff
    margin-bottom 15px
    h1
        margin 0
        padding 0
    a
        text-decoration none
        color #333333

footer
    font-size 10px
    color #ffffff
    text-align center
    padding 10px 0
    background-color #000000

#contents
    background-color #ffffff
    max-width 1200px
    width 100%
    margin 0 auto 30px auto
    padding 30px 10px

nav
    width 25%
main
    width 75%

table
    border-collapse collapse
    border-top 1px solid #cccccc
    border-left 1px solid #cccccc
    width 100%
    th
        background-color #eeeeee
        text-align left
        padding 10px
        white-space nowrap
        font-size 14px
        border-bottom 1px solid #cccccc
        border-right 1px solid #cccccc
    td
        padding 10px
        font-size 14px
        border-bottom 1px solid #cccccc
        border-right 1px solid #cccccc

ul,li
    padding 0
    list-style none

ul
    width 100%
    max-width 1200px
    margin 0 auto 10px auto

    li
        color #ffffff
        display inline-block
        &:after
            content ">"
            display inline-block
            margin-left 10px
    li:last-child:after
        display none
    li + li
        margin-left 10px
    a
        color #ffffff

変換コマンド

Stylus変換

npm run gulp stylus

ejs変換

npm run gulp ejs

Watch

npm run gulp watch

リンク

Github

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