- 投稿日:2019-12-03T22:48:14+09:00
【自分用メモ】supertestとpassport-stubをmochaテストに組み合わせる
supertestとは
supertestはmochaと組み合わせて使うのですが、ExpressのRouterモジュールのテストを行うことができます。
例えば以下の例では、/
にアクセスしたらindexRouterが処理されるかテストしてくれます。
もちろん、/login
も/logout
もテストしてくれます。app.jsapp.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); }); });
describe
、it
、before
、after
はmochaの書き方。
request(app).get
、.expect
はsupertestの書き方
passportStub.install(app)
、passportStub.login
はpassport-stubの書き方テスト結果
- 投稿日:2019-12-03T22:19:49+09:00
decoratorとライブラリを紐解き、軽量 DI コンテナを自作しよう
なぜ DI コンテナを自作するか
関心の分離がされているアプリケーションは変更に強く、良い設計と言えます。Dependency Injection(以下 DI) は関心の分離を実現する テクニックの 1 つとしてよく見られるパターンです。しかしクラス間の依存関係が増えれば増えるほど、注入する依存を作ることは困難になり、DI のコストは段々と膨らみます。そのようなとき、 依存を自動で解決し、欲しいインスタンスをすぐにとりだせる DI コンテナ は有効な解決手段となり得ます。
JavaScript/TypeScript においても DI コンテナを提供するライブラリが存在します。例えば、InversifyJS や tsyringe などが知られています。しかし既存の 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 コンテナだと思っており、お勧めします。
- 投稿日:2019-12-03T22:19:49+09:00
DIコンテナの実装を理解して、軽量 DI コンテナを自作しよう
なぜ DI コンテナを自作するのか
関心の分離がされているアプリケーションは変更に強く、良い設計と言えます。Dependency Injection(以下 DI) は関心の分離を実現する テクニックの 1 つとしてよく見られるパターンです。しかしクラス間の依存関係が増えれば増えるほど、注入する依存を作ることは困難になり、DI のコストは段々と膨らみます。そのようなとき、 依存を自動で解決し、欲しいインスタンスをすぐにとりだせる DI コンテナ は有効な解決手段となり得ます。
JavaScript/TypeScript においても DI コンテナを提供するライブラリが存在します。例えば、InversifyJS や tsyringe などが知られています。しかし既存の 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 コンテナだと思っており、お勧めします。
- 投稿日:2019-12-03T19:31:50+09:00
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 --helpnvmのバージョンを表示する
$ 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も手軽にインストールできます。
参考リンク
- 投稿日:2019-12-03T19:05:48+09:00
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.jsconst 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.txtabcde fghij klmno pqrxy z実行結果
terminalabcde fghij klmno pqrxy z
従来だと、下記のように終了イベントをpromise化するなどで、全体のpromise化などは簡単にできますが、イテレータ内部のchunk単位でpromise化を行う場合非常に可読性が悪くなってしまっていました。
(v10以降であれば下記のようにstream.finishedをpromisifyすることで全体のpromise化は簡略可能です)これが上記のように簡単に記述できるようになったのは非常にやりやすくなったと感じます。
for-await-of_loops2.jsconst 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); }); })();実行結果
terminalabcde fghij klmno pqrxy z abcde fghij klmno pqrxy z
async-generators
今までは同期メソッドでしか使えなかったyieldがasyncにも対応しました。
async function*
でasyncIteratorのジェネレータメソッドを生成でき、await対応したnextメソッドを呼び出すことができます。
(nextで呼び出した場合返り値はObjectになります)async-generators1.jsconst 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.jsconst 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.jsconst 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); }); })();実行結果
terminalabcde fghij klmno pqrxy z
解説
こちらはreadlineモジュールを利用したものになります。
以前も行ごとに処理を行えたのですが、非同期メソッドの実行はできませんでした。
v1.12からasyncIteratorに対応したことで、上記のように簡単に非同期メソッドが実行できるようになりました。stream(for-await-of利用)
line-reader2.jsconst 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); }); })();実行結果
terminalabcde fghij klmno pqrxy z
解説
こちらはstreamのfor-await-ofを利用したものになります。
イベントループの回数が他と比べて半分以下なので、このなかでは一番パフォーマンスが良いです。
実装を合わせるために1行ずつ処理していますが、こちらで複数行制御してlistをiteratorに渡すような実装が実利用だと良いかもしれません。
比較的簡単に可読性良く記述できるようになっているかと思います。stream(for-await-of未使用)
line-reader2.jsconst 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); }); })();実行結果
terminalabcde fghij klmno pqrxy z
解説
こちらはstreamのfor-await-ofの未使用版になります。
かなり複雑になり可読性も落ちている事がわかります。
ただ、async, awaitが対応しているv8以降であれば利用可能なため、nodeのバージョン次第では利用できるかもしれません。まとめ
これらの機能の追加により、async, awaitを用いたstream処理が非常に簡潔に記述できるようになりました。
v1.12LTSに上げることで非常にstream周りが記述しやすくなっているため、この機会にぜひ試してみてはどうでしょうか?
- 投稿日:2019-12-03T17:36:25+09:00
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/ )というのもあるらしいです。
手順
- グローバルにavnとavn-nodebrewをインストール&セットアップする。
terminalnpm install -g avn avn-nodebrew avn setup
- プロジェクトのルートディレクトリに
.node-version
を作成する。 作成したファイルには使いたい node.js のバージョンを以下のようにセマンティックバージョニングで書く。.node-versionv6.17.1
- ターミナルでプロジェクト内に
cd
で移動して、以下のようにactivated
が表示されていればOK。terminalavn activated v6.17.1 (avn-nodebrew v6.17.1)2. Trouble Shooting
だいぶ完結に「1.」にまとめましたが、実際は結構時間がかかりました。
やったことで覚えていることを時系列に関係なくメモだけしておきます。
node -v
をして確認したら切り替わってなかった↑のようにバージョンが変わったように思ったけど、
node -v
で念のためバージョンを確認すると変わっていなかった...。
とりあえず、 一旦全部削除してみようと思い、一からやり直してみた。terminalv10.17.0pkg で入れた node.js を削除(ついでに npm も削除)
MacにpkgでインストールしたNode.jsをアンインストールする手順を参考に削除。
以下を一行ずつ実行していく。(>がでてきても次の行を>につづけてペーストしてenterを押す)terminallsbom -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 も削除する場合は以下で。
terminalsudo rm -rf ~/.npmnode.js、npm が削除されてるかどうかは以下のコマンドでチェック(バージョンがでてこなければOK)
terminalnode -v npm -vnodebrew を入れ直してみる
nodebrew を削除する
Nodebrew本体を削除する方法を参考に Finder or ターミナルから直接
.nodebrew
ディレクトリを削除。
(隠しファイルの表示方法はこちら)ただ、homebrew で nodebrew を入れた場合は上記じゃ消せないときがあるので、その場合は下記のコマンドで削除。
terminalbrew uninstall nodebrewnodebrew を入れ直す
GitHubページの通り、curlコマンドでインストール。
(homebrew でも入れれますが、削除するときにちょっと面倒だったのでcurlで入れました)terminalcurl -L git.io/nodebrew | perl - setup
インストールしたら、パスを通す。(GitHubページには
.bashrc
or.zshrc
と書かれてます)
私は.bashrc
に書きました。export PATH=$HOME/.nodebrew/current/bin:$PATHbashrcを更新しても、ターミナルを再起動しただけではシェルの設定が反映されないので、以下のコマンドを叩いて反映させる。
terminalsource ~/.bashrc
!!注意!!
上記を実行しただけだと
「PhpStormのTerminalでは nodebrew コマンドが使えるのに、App のターミナルで新しいタブを開いても nodebrew コマンドが使えない...」
という状況に陥りました。これは実行系統によって読み込む設定ファイルが微妙に異なることが原因だそう。(両者のターミナルで読み込む設定が違ったみたい)
そこで、.bashrc
の更新内容が今後自動的に.bash_profile
に反映されるようにするため、.bash_profile
に下記コマンドを追加するとどちらでも nodebrew コマンドが使えるようになりました。.bash_profilesource ~/.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 にインストール済みのバージョンの確認
terminalnodebrew ls
nodebrew にバージョンをインストールする
terminalnodebrew install-binary 6.17.1参考
avnのGitHubページ
Nodebrewとavnを使ってNode.jsのバージョン切り替えを自動化する
ディレクトリごとに異なるバージョンのnodeを使いたいのでavnを使った話
- 投稿日:2019-12-03T17:05:29+09:00
初心者に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れなくていいじゃないか!」「とりあえず単純にして慣れてもらおう!」ということでパッケージを作ってしまいました。
それがmongodbeginner(GitHub)です。
基本的に内部で教える際に使うために作ったので色々と雑で実用に耐えるものかは怪しいです。使い方
mongodbeginnerでは初心者が とりあえず DBを使えるように工夫した結果、接続などの処理を毎回行います。
例えばid
が1
のデータをfindしたい際にはmongodbeginnerのサンプルconst mob = require('mongodbeginner') const data = await mob.find("dbName","collectionName",{id: 1})とすればOkです。
接続して......ってのが複雑なのが解決したのではないでしょうか?
もし{id: 1, count: 0}
というデータをinsertしたい場合はmongodbeginnerのサンプル2const 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でもやろうかなぁ。時代遅れって聞くこともあるけど...
- 投稿日:2019-12-03T17:02:31+09:00
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/ からアプリケーションの作成を行なってください。
作成ができたら、アプリの設定ページで以下のように、アプリIDとapp secretが作成されています。
このアプリ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 starthttp://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.js
はpassport
モジュールの設定などを書き込むファイルです。
・localhost:8000/
にアクセスしたときの処理を書き込みます。
・login.js
とlogout.js
は、/login
と/logout
にアクセスしたときの処理を書き込みます。
・www
はサーバーの起動などを担当します。
・login.pug
は/login
にアクセスした時のログイン画面を表示します。
・index.pug
は/
にアクセスした時の画面を表示します。
・layout.pug
はlogin.pug
とindex.pug
の基本となる表示を担当します。続いて、以上のような認証の際に必要なファイルやディレクトリの作成、その記述を行なっていきます。すでに存在しているファイルの作成は不要なので、以下のファイルだけを作成します。
console$ touch routes/login.js $ touch routes/logout.js $ touch views/login.pugファイルに処理の記述を行う。
続いては、処理の記述を行なっていきます。
まずは、今あるapp.jsの記述を削除して、app.jsに以下の処理を記述してください。app.jsvar 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';
は、先ほど作成した、アプリIDとapp secretをそれぞれ代入します。
var FACEBOOK_APP_SECRET = '170dde4751f2exxxxxxxxxx';続いて、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.pugextends 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.pugextends layout block content a(href="/auth/facebook") Login with Facebookログインするための
a(href="/auth/facebook")
を追加しました。ログインとログアウトをしてみる
これで処理か完成しました。コンソールにて以下のコマンドを入力し、
consolePORT=8000 npm start
以下のURLにアクセスしてみてください。
http://localhost:8000/すると、
/login
に移動するので、Login with Facebook
をクリックします。
すると、ログイン画面に出るので、
続いて、自分のFacebookアカウントでログインボタンをクリックし、ログインします。
ログインすると、以上のように、Hello 自分の名前
と表示されます。
続いて、Logout
をクリックし、ログアウトできるかの確認も行います。無事、最初のログイン画面に移動できたら、ログアウトの完了です。
お疲れ様でした。おまけ
認証された人しか見れないようにするには、app.jsに以下のように書き込みます。
app.js//認証者を確かめる関数 function authenticatedUser(req, res, next) { //認証されている人は次の処理が実行される。 if (req.isAuthenticated()) { return next(); } //認証されてない人は`/login`にリダイレクトされる。 res.redirect('/login'); }
/users
を認証者だけが見えるように設定。app.jsapp.use('/users', authenticatedUser, usersRouter);
- 投稿日:2019-12-03T17:01:26+09:00
Node8系→12系にあげたらいっぱいエラー出た
概要
お世話になったnode8系が2019/12に寿命を迎えます
https://github.com/nodejs/Release#release-schedule
そのため、node (v8.7.0) → (v12.13.0)に変更しましたが、いろいろとエラーが出たためそのメモ書きです。
(使っているmoduleによって個人差が出るため完全に自分のための覚書です)アップグレード方法
$ 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に対応しているか確認
https://github.com/sass/node-sassNodeに対応しているモジュールのインストール
$ 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.json
にresolutions
フィールドを書くことで、
特定のモジュールに依存しているモジュールのバージョンも指定できます解決
無事解決しました
$ 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 ~~ sNodeの知識全然足りないので勉強しなきゃなーと考えさせられました
参考サイト
GitHub Issues
- Fails to build with node.js v12.0.0 release (https://github.com/grpc/grpc-node/issues/834)
- yarn fails to install fsevents while npm succeeds (https://github.com/yarnpkg/yarn/issues/5962)
- support for node.js 12.x (https://github.com/sass/node-sass/issues/2632)
resolutionsについて
- 投稿日:2019-12-03T16:36:06+09:00
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
などを使った方がいいです。
- 投稿日:2019-12-03T13:44:58+09:00
公式インストーラーからインストールしたNode.jsを削除する方法(macOS)
概要
Node.js FoundationはMacOS向けに公式インストーラーを用意している。
アンインストールの方法についての公式の見解が特に見当たらなかったので、推測しながら試した結果うまくいったので、記録として残す。
もし間違っている点があれば教えてほしいです!対象者
- Node.jsの公式インストーラーをインストールしたが、nvmなどのバージョン管理ツールに切り替えたい人
- 既にnvmなどのバージョン管理ツールを使用しているが、過去にNode.jsの公式インストーラーを使用した覚えのある人
背景
興味本位から、まっさらなMacOSに公式インストーラーでNode.jsをインストールしたところ、
「これどうやってアンインストールするんだ...?」となったため。アンインストール方法
まず、Node.jsインストール後、以下のようなメッセージが出ることをご存知だろうか。
つまりこういうことですね。
Node.jsを
/usr/local/bin/node
にインストールしたで!
npmを/usr/local/bin/npm
にインストールしたで!つまり、このインストーラー経由でインストールしたNode.jsは、
/usr/local/bin/node
を削除することでアンインストールできるということです。(という推測を立てた)
というわけで、以下のコマンドを実行。rm -f /usr/local/bin/nodenpmをアンインストールしたい場合も同様。
rm -f /usr/local/bin/npmさいごに
既にバージョン管理ツールを用いてNode.jsを管理している場合でも、そもそもNode.jsの格納先が違うのでこの方法で対処できるかと思います。
(nvmにて検証済み。保証はしません)多少知識があればバージョン管理ツールを使うのが良いと思いますが、初学者にNode.jsをインストールしてほしい!けどアンインストール方法も知っておいてほしい!という場面などでご活用ください。
- 投稿日:2019-12-03T13:36:17+09:00
【Webpack4 備忘録】オススメ設定詰め合わせ
備忘録で作成したもの以外に入れた方がいいもののまとめ。
以前、備忘録で作成した こちら に設定を追加する。
目次
ソースマップにオリジナルのソースコードを表示する
デフォルト設定のソースマップは以下の通り。
このままだと、デバッグが捗らない。
開発環境で、ソースマップにオリジナルのソースコードを表示する設定を追加するには、webpackにdevtoolの設定を追加する必要がある。
実装
webpack.conf.jsに以下の設定を追加する。
// 中略 ... module.exports = { // 中略 ... // ソースマップの設定(オリジナルのソースコードを出力、コメントも出力される) devtool: 'eval-source-map', // 中略 ... };変更後は、以下の通りオリジナルのソースコードが表示されるようになる。
これで多少はデバッグがしやすくなるはず。
参考
build時にhtmlファイルも生成する
以前作成したボイラープレートのままだと、
build
ディレクトリにHTMLファイルは生成されない。
html-webpack-plugin
を使用することで、ビルドコマンドを実行するたびにbuild
ディレクトリにHTMLファイルを生成できるようになる。html-webpack-plugin とは
webpackで生成したJavaScriptやCSSを埋め込んだHTMLを生成できるプラグイン。
ディレクトリごとデプロイしたい場合などに使える。
実装
プラグインをインストールする。
$ npm install -D html-webpack-plugin
webpack.config.js
でhtml-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-loader
とcss-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 modulesCSSファイルが別ファイルとして切り出されていることがわかる。
$ 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 modulesbuildディレクトリを削除して、再作成するため
test.txt
は存在しない。$ tree ./build ./build ├── bundle.js ├── images │ └── react.png ├── index.html └── main.css 1 directory, 4 filesプロダクション環境へのデプロイモジュールを小さくする
ビルドコマンドで
production
を指定している場合、ソースコードは最適化されるが、一部不要なコードやCSSの最適化までは行われない。
uglifyjs-webpack-plugin
とoptimize-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を使用する。
実装
exlint
とprettier
とその他必要なプラグインをインストールする。$ 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" }, // 中略 ... }
今回作成したものは こちら
- 投稿日:2019-12-03T13:03:35+09:00
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
になります
- 投稿日:2019-12-03T12:33:41+09:00
Nodebrew 本体が削除できなかった話
Nodebrew本体を削除する方法 に従い、Finder から削除したのに
terminal$ nodebrew -vでバージョンが表示され続け、消せていないようだった。
解決法
Homebrew で入れた nodebrew が残っているのが原因だったよう。
以下のコマンドを実行すると、
nodebrew -v
でもバージョンが表示されなくなり、nodebrew の削除が完了した。terminal$ brew uninstall nodebrew
参考にした記事
- 投稿日:2019-12-03T09:43:36+09:00
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.jsimport 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側についてはそのままありのままなので詳しくは説明できません。
見てくれた方には心苦しいですが、赦してほしいです。
では、また。
- 投稿日:2019-12-03T09:38:48+09:00
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.jsimport 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側についてはそのままありのままなので詳しくは説明できません。
見てくれた方には心苦しいですが、赦してほしいです。
では、また。
- 投稿日:2019-12-03T06:53:20+09:00
typescriptで書かれたnodeアプリ、クライアント サーバ どっちでコンパイルする?
- 投稿日:2019-12-03T01:22:22+09:00
Paiza Cloud で LINE Bot を試してみる
プロトアウトスタジオアドベントカレンダー4発目の記事です!
昨日は @tkyko13 さんの「word2vecの勉強で「word2vecの勉強で「ナダルリバースエボリューション」が再現できるのではないかと思いついたのでやってみた」でした。
Paiza Cloud とは
クラウド開発環境 PaizaCloudクラウドIDE - クラウドIDEでWeb開発!
ブラウザを開くだけでLinuxサーバが使える!
クラウド開発環境PaizaCloudクラウドIDEでは、ブラウザだけでLinuxサーバを操作できます。ファイル操作、テキスト操作、コマンド操作、Webサーバ/DBサーバの立ち上げなど、全てブラウザだけで行えます。 もう、面倒なコマンドでのログイン(ssh)やファイル操作(vim)、ファイルのアップロードは必要ありません。 目の前のコンピュータと同じように、クラウド上のLinuxサーバを操作できます。とのことで、以前、Katacodaで LINE Messaging API Playground (ja) を作った身としては興味があります。
料金表をみてみても1つのサーバーが無料で24時間使えるので、Katacodaよりも長時間使えます。(2019/12/03現在)
サーバーを立ち上げる
Paiza Cloud のアカウント登録をして、まず、Node.jsサーバを立てあげてみましょう。
新規サーバーを押します。
Node.jsをクリックして新規サーバ作成します。
このような形で起動します。
LINE Bot をつくる
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest - Qiita
こちらの資料をベースに、「1. Botアカウントを作成する」を進めてBotと友達になるところまで進めましょう。
「2. Node.jsでBot開発」からはじめます。
左のメニューのターミナルを押して、ターミナルを起動します。
npm i @line/bot-sdk expressを実行します。
無事インストールされました。
新規ファイルを押して、
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}`);保存します。
自分の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です。
公開URL確認
起動すると左のメニューに 3000 というボタンが出来るので、公開URLを確認します。
内部ブラウザが開いてURLが確認できるのでメモしておきましょう。
こちらをLINEの管理画面のメッセージ送受信設定>Webhook URLに反映します。
これで準備完了です。
動かしてみる
実際にLINEでBotを会話してオウム返しを体験しましょう。
LINEで話しかけてみると無事返答されます。
ターミナルを見てみると
サーバーのやり取りも確認できます。
明日の記事は…
@doikatsuyuki さんの「Firebaseを利用した中耳炎診療支援Webアプリの作成 (1.Firebaseの設定~認証方法の追加)」です!
- 投稿日:2019-12-03T00:55:59+09:00
LINEログインを、Node.js + Herokuでやってみた
お仕事でLINEログインをチームで実装する機会があったので、自分でもできるように小さなサービスを作ってみようと思います。
まずは、LINEログインボタンを表示し、それを押すとLINEログインの実行を行い、ログインしたアカウント情報を表示するところまで実装します。
サーバサイドはNode.jsを利用し、インフラとしてはHerokuを利用します。
(LINEからのcallback URLを指定する必要があるので、Herokuが便利です。)事前準備
Node: v12.13.1
Heroku CLI: 7.35.0Nodeアプリ環境の構築
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/d6e3f23e4a533147132c4ee613e57de746085d39codeから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_token
とrefresh_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。今回は参考として。
- 投稿日:2019-12-03T00:35:11+09:00
CSVデータから大量にHTMLファイルを生成する。
概要
神戸市が提供しているオープンデータ「観光施設情報」のCSVを元にHTMLを量産します。
制作物
・観光地一覧ページ(index.html)
・観光地詳細ページ(place**.html)
開発環境
node v9.3.0
npm v6.12.1
gulp v4.0.2package.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,バリアフリー対応ソース
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 stylusejs変換
npm run gulp ejsWatch
npm run gulp watchリンク