typescript導入したprivateなnpmパッケージの作り方

はじめに

開発の規模を大きくなってくると、共通化したコンポーネントを利用したいこともあると思います。
git submoduleをつかって共通部分を切り出すことも可能ですが、branchの変更忘れてしまうと反映されないので、個人的には好みではないです。 一方privateなnpmパッケージで実現することも可能だと思います。

npm private registoryを利用することも可能ですが、こちらの場合 US $7/月という月額料金がかかってしまいます。 一方、github privateは無料になったので、githubをつかってprivate npmパッケージを作ってみたいと思います。

また共通利用するなら型情報があったほうがありがたいので、typescriptを導入してみたいと思います。

目標

  1. github privateリポジトリの作成
  2. 他のリポジトリからimport確認
  3. npm version を利用してバージョン管理する

1. my private npm moduleの作成

npm moduleの作成

共通ライブラリのnpmパッケージを作っていきます。

  1. npm moduleの作成
mkdir myNpmModule
cd myNpmModule
git init
npm init -y

.gitignoreの設定は割愛します。

  1. typescript設定
npm i -D typescript
npx tsc --init

npx tsc --init で自動生成された.tsconfig.jsonに以下追加してコンパイル設定を修正する。

.tsconfig.json

 {
 ...
 "declaration": true,
 "sourceMap": true,
 "outDir": "./build",
 "rootDir": "./src",
 ...
}

rootDir でbuild対象のファイルを指定し、outDir でbuild後のファイル保存先を指定する。

declaration でビルド時にxxx.d.tsとして型定義ファイルを出力させる。
sourceMap でtsとjsの対応関係をつくっておく。

  1. package.json修正

共通ライブラリの設定を追加します。

package.json

{
  ...
  "main": "build/index.js",
  "files": [
    "build"
  ],
  "scripts": {
    "build": "tsc",
    "prepare": "npm run build"
  },
  ...
}

files ではパッケージ利用側にどのディレクトリ・ファイルをダウンロードさせるかを指定できる。
ただし、package.jsonとREADME.mdは指定しなくてもダウンロードされる。

main でパッケージ利用側がimportコマンドを使用したときにどのファイルをロードするのかを指定する。

scripts では、typescriptのbuildコマンドに加えて、 prepare を設定する。

prepare はパッケージ利用側が、パッケージをインストールする直前に実行させる処理を定義できる。
srcからビルドされたファイルを作成してくれるため、パッケージ管理側はbuildファイルをgit管理しなくてよい。

つまり、利用側で勝手にsrcからbuildディレクトリを生成してくれるため、.gitignoreにbuildディレクトリを指定しなくてよいわけとなります。

  1. 公開モジュール作成
mkdir src
touch src/Person.ts

src/index.ts

class Person {
  private name: string;
  private age: number;

  public constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public call(): string {
      return this.name;
  }
}

export { Person };
git add .
git commit -m 'my first commit';

github private repoとして登録

githubページより、新しいリポジトリ作成から、privateにチェックをつけて作成する.

f:id:poppon555:20200405122203p:plain

git remote add origin git@github.com:karuta0825/myNpmModule.git
git push -u origin master

f:id:poppon555:20200405122217p:plain

buildディレクトリ管理しなくてよしです。

2. 他のリポジトリからimport確認

package.json

{
  "dependencies": {
   ...
   "myNpmModule": "git+ssh://git@github.com:karuta0825/myNpmModule.git",
   ...
  },
}

利用側のpackage.jsonのdepenenciesで、使用したいprivate moduleを指定する。git+ssh//に続けて、githubのclone or dowloaddに表示されているパスをコピーすればよき。

f:id:poppon555:20200405122242p:plain

npm i を実行すると、node_modulesに作成したprivate packageが作成されてます。

.
├── node_modules
│   ├── @types
│   ├── myNpmModule <-- 追加されてる
│   └── typescript
├── package-lock.json
├── package.json
├── sample.ts
└── tsconfig.json

node_modulesのmyNpmModuleのディレクトリ構成も設定したとおり。

myNpmModule
├── build
│   ├── index.d.ts
│   ├── index.js
│   └── index.js.map
└── package.json

package.jsonのfilesに指定したbuildディレクトリのみがあり、そこには、型定義ファイルのindex.d.tsとコンパイルされたjsとsourceMapの.mapファイルがある。

では、実際にインストールした共通パッケージを利用できるか確認してみます。

f:id:poppon555:20200405123146p:plain importでき、候補も表示されています。

3. my npm moduleをバージョン管理する

共通パッケージを使用していくならば、バージョン管理をしておくも考えられると思います。
ということで、npmのバージョン管理設定も説明しておきます。
とっても簡単。commit後に npm version を使用するだけです。

import dayjs from "dayjs";

class Person {
  private name: string;
  private age: number;
  public constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public call(): string {
    return this.name;
  }

  // 追加
  public tellBirthYear(): string {
    return dayjs().subtract(this.age, "year").format("YYYY");
  }
}

git commitする

git add .
git commit -m 'add a method to Person class.'

npm versionのあとに、major, minor, patchのいずれかを指定できます。
majorだと、1.0.0 => 2.0.0
minorだと、1.0.0 => 1.1.0
patchだと、1.0.0 => 1.0.1
にバージョンが変わる。

package.jsonのversionや、git logが変更されていること確認できます。

npm version minor

git log --oneline
// 6a0bb79 1.1.0
// 3d505ff add a method to Person class.
// 351675f my first commit

package.json

{
  ...
  "version": "1.1.0",
  ...
}

git pushして、利用側でnpm updateをして確認すると、

npm update

+ myNpmModule@1.1.0
added 1 package from 1 contributor, updated 3 packages and audited 5 packages in 19.281s
found 0 vulnerabilities

1.1.0のmyNpmModuleが利用できるようになりました。

実際下図の通り、追加したメソッドが候補に上がるようになります。

f:id:poppon555:20200405122235p:plain

まとめ

github privateを利用して、typescript導入npmパッケージを作成しました。
また npm version を利用してバージョン管理する方法をかんたんに説明しました。

ライブラリとして使用されるならば、以下のことも今後対応方法見つけられたらと思ってます。

  • パッケージのmin化 現状だとsrcのディレクトリ構成のままbuildディレクトができるので、一本にまとめたほうがよい。
  • material-uiやlodashのようにmyNpmModue/Personみたいな感じでロードファイルを指定できる

参考にしたサイト

https://dev.classmethod.jp/articles/private-npm-modules-to-package-json/https://medium.com/cameron-nokes/the-30-second-guide-to-publishing-a-typescript-package-to-npm-89d93ff7bccd

TypescriptでSequelize してみる

はじめに

前回 Sequelize の一番基本的な使い方を紹介したが、typescript で記述可能ならば、以降 ts で書きたいなと思ったので、今回は typescript の環境を紹介したいと思う。

ts で記述するメリットは、型が使えることだけではなく、エディタからの候補表示である。property や method の打ち間違いも ts ならば防げるわけだ。

目標(課題)

公式の typescript 導入紹介では、一つのファイルに Model 定義・初期化・associate・main ルーチンなどをすべて記述する。

実運用では、各 Model は models ディレクトリで管理するものだろう(sequelize-cli が初期化時に models ディレクトリを作るぐらいだ)。

なので、ただ ts 環境を作成するだけではなく、models ディレクトリで Model を管理し、main ルーチンは、main ファイルに記述する環境を構築したいと思う。

ディレクトリ構成

はじめてに最終的なディレクトリ構成や実行イメージを紹介する。

.
├── config.json          # db 接続情報
├── package.json
├── src
│   ├── main.ts          # mainルーチン
│   └── models           # Model管理
│       ├── index.ts
│       ├── project.ts   # projectモデル
│       └── task.ts      # taskモデル
└── tsconfig.json

起動方法は、ts-node を利用してnpx ts-node src/main.tsで可能とする。

インストール

Sequelize

npm -i sequelize mysql2

Typescript

npm -i -D typescript @types/node @types/validator @types/bluebird ts-node

npx typescript --initで tsconfig.json を作成して、好きな設定をしてください。

src ディレクトリに main.ts ファイルを作成して、npx ts-node src/main.tsがうまく動作することを確認できれば OK です。

model の定義

Model は、Project, Task を作成する。 それぞれの仕様は下図の通り。

f:id:poppon555:20190721132919p:plain

両 Model とも id をプライマリキーにもち、title と description に加えて createdAt, updateAt をフィールドにもつ(createdAt, updateAt は sequelize で自動で作成されるので気にしなくてよい)。

さらに Project は複数の Task をもつ関係にあり、projectId が外部キーとなっている。

これで準備が整ったので実装に入ってみる。

src/models/project.ts

import { Sequelize, Model, DataTypes } from 'sequelize';
import { HasManyCreateAssociationMixin } from 'sequelize';
import Task from './task';

export default class Project extends Model {
  public id!: number;
  public title!: string;
  public description!: string;

  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  // (3) 作成したprojectをprojectIdをもつtaskを作成するメソッド
  public createTask!: HasManyCreateAssociationMixin<Task>;

  // (1)初期化
  public static initialize(sequelize: Sequelize) {
    this.init(
      {
        id: {
          type: DataTypes.INTEGER.UNSIGNED,
          autoIncrement: true,
          primaryKey: true
        },
        title: { type: DataTypes.STRING },
        description: { type: DataTypes.TEXT }
      },
      { sequelize, tableName: 'project' }
    );
    return this;
  }

  // (2)テーブル関係を記述
  public static associate() {
    this.hasMany(Task, {
      sourceKey: 'id',
      foreignKey: 'projectId',
      constraints: false
    });
  }
}

クラスプロパティ

sequelize の Model を継承する class を作成して export させる。クラスプロパティには、定義したフィールドを記述してやる。

しかしこの記述は typescript 用であり、sequelize が利用するには、次の initialize メソッドが必要になる。

static メソッド(initialize)

このメソッドは、引数に sequelize を受け取っているように、DB 接続設定完了した Sequelize インスタンスをもとに、実際に初期化作業を行っている。

typeプロパティやtableNameプロパティなど 前回記述した js の定義と若干異なるので注意が必要である。

static メソッド(associate)

このメソッドは、Project と Task Model の関係を sequelize に知らせるためのものである。thisは Project クラス自身を指して、第一引数にTaskを受け取る。

第二引数には、関係の詳細情報を与える(任意)。

  • foreinKey
    hasMany される側(Task)の外部キーを指定する 指定しない場合、ProjectId と大文字になる
  • sourceKey
    source は、hasMany の主体(Project)側の外部キーとつながるキーを指定する。 なてくもよい。
  • constraint
    制約情報(外部キー)の有効化フラグ Project.sync({ force: true })を動作させるために false に設定。

createTask メソッド

HasManyCreateAssociationMixinという型情報があってわかりづらいが、このメソッドは Project が作成された後、それに紐づく Task を作成できるものである。

なぜ static メソッドにするのか?

Sequelize の Model クラスは、save, update, remove メソッドの結果を受け取ったときインスタンスであるが、findAll などインスタンスを検索したり、作成するときはインスタンスではなくクラスの static メソッドで行っている。

1 レコード情報をもつのが、Model インスタンスであり、テーブル全体の情報をもつのが Model クラスなわけだ。

だから、createTask のように作成された project に紐づく task を作成するなばら、通常のメソッドになり、一方テーブル定義したり、テーブル同士の関係を記述するのは、static メソッドとなる。

同じように Task モデルも作成する

src/models/task.ts

import { Sequelize, Model, DataTypes, Association } from 'sequelize';
import Project from './project';

export default class Task extends Model {
  public id!: number;
  public title!: string;
  public description!: string;
  public deadline!: Date;
  public projectId!: number;

  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  public static initialize(sequelize: Sequelize) {
    this.init(
      {
        id: {
          type: DataTypes.INTEGER.UNSIGNED,
          autoIncrement: true,
          primaryKey: true
        },
        title: { type: DataTypes.STRING },
        description: { type: DataTypes.TEXT },
        deadline: { type: DataTypes.DATE },
        projectId: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false }
      },
      { sequelize, tableName: 'task' }
    );
    return this;
  }

  public static associate() {
    this.belongsTo(Project, { foreignKey: 'projectId', constraints: false });
  }
}

モデルをまとめる

モデル定義が終わったので、次はこれら model をまとめる index.ts を作成しよう。

src/models/index.ts

import { Sequelize, Model } from 'sequelize';
import Project from './project';
import Task from './task';
const { database, username, password, host, dialect } = require('../../config');

// sequelizeインスタンス作成
const sequelize = new Sequelize(database, username, password, {
  host,
  dialect
});

// (1)モデルを一つのオブジェクトにまとめる
const db = {
  Task: Task.initialize(sequelize),
  Project: Project.initialize(sequelize)
};

// (2)テーブル同士の関係を作成する
Object.keys(db).forEach(tableName => {
  const model = db[tableName];
  if (model.associate) {
    model.associate();
  }
});

export default db;

(1)複数のモデルをひとつのオブジェクトにまとめる

dbというオブジェクトに作成した Project, Task を一括管理させる。sequelize インスタンスを渡して、初期化された Project, Task モデルを管理する。

(2)テーブルの関係づけ

static メソッドの associate をもつならば、それを呼び出してテーブル同士を対応づけている。

main

ここまで来たらあとは簡単ですね。 typescript の恩恵を受けれるので打ち間違いもないだろうし。

src/main.ts

import models from './models';

(async () => {
  // Project, TaskテーブルをDrop & Create
  await models.Project.sync({ force: true });
  await models.Task.sync({ force: true });

  // projectインスタンス作成
  const project = models.Project.build({
    title: 'my awesome project',
    description: 'woot woot. this will make me a rich man'
  });

  // projectのinsert
  const createdProject = await project.save();

  // insertされたprojectにひもづくTaskを作成
  await createdProject.createTask({
    title: 'title',
    description: 'description'
  });

  // projectのselect
  const projects = await models.Project.findAll({
    include: [models.Task]
  });
  console.log(projects.map(d => d.toJSON()));

  // taskのselect
  const tasks = await models.Task.findAll({
    include: [models.Project]
  });
  console.log(tasks.map(d => d.toJSON()));
})();

npx ts-node src/main.tsを実行すれば下記のように出力されるであろう。

Executing (default): SELECT `Project`.`id`, `Project`.`title`, `Project`.`description`, `Project`.`createdAt`, `Project`.`updatedAt`, `Tasks`.`id` AS `Tasks.id`, `Tasks`.`title` AS `Tasks.title`, `Tasks`.`description` AS `Tasks.description`, `Tasks`.`deadline` AS `Tasks.deadline`, `Tasks`.`projectId` AS `Tasks.projectId`, `Tasks`.`createdAt` AS `Tasks.createdAt`, `Tasks`.`updatedAt` AS `Tasks.updatedAt` FROM `project` AS `Project` LEFT OUTER JOIN `task` AS `Tasks` ON `Project`.`id` = `Tasks`.`projectId`;

[ { id: 1,
    title: 'my awesome project',
    description: 'woot woot. this will make me a rich man',
    createdAt: 2019-07-21T04:23:17.000Z,
    updatedAt: 2019-07-21T04:23:17.000Z,
    Tasks: [ [Object] ] } ]

Executing (default): SELECT `Task`.`id`, `Task`.`title`, `Task`.`description`, `Task`.`deadline`, `Task`.`projectId`, `Task`.`createdAt`, `Task`.`updatedAt`, `Project`.`id` AS `Project.id`, `Project`.`title` AS `Project.title`, `Project`.`description` AS `Project.description`, `Project`.`createdAt` AS `Project.createdAt`, `Project`.`updatedAt` AS `Project.updatedAt` FROM `task` AS `Task` LEFT OUTER JOIN `project` AS `Project` ON `Task`.`projectId` = `Project`.`id`;

[ { id: 1,
    title: 'title',
    description: 'description',
    deadline: null,
    projectId: 1,
    createdAt: 2019-07-21T04:23:17.000Z,
    updatedAt: 2019-07-21T04:23:17.000Z,
    Project: 
     { id: 1,
       title: 'my awesome project',
       description: 'woot woot. this will make me a rich man',
       createdAt: 2019-07-21T04:23:17.000Z,
       updatedAt: 2019-07-21T04:23:17.000Z } } ]

さいごに

ts 環境の構築と sequelize の説明両方が入ってしまったので、ややこしくなってしまった感いなめないが、何かの参考になればと思う。

associateなど sequelize の使い方に関する説明は今後ちゃんと記述して説明していこうと思う。

Sequelize入門

SequelizeというORMについて知識の整理のため、記録を残そうと思う。Databaseはmysqlを使用する。

今回は、
1. database作成
2. table作成
3. insert, update, delete
という基本的なことの整理をしたい

1. databaseの作り方

databaseを作成するのは、sequelizeではなく、sequelize-cliを利用するので、両方インストールする。

npm i -D sequelize-cli sequelize mysql2

以下のファイルを作成する。

config.json

{
  "username": "root",
  "password": "****",
  "database": "sequelize",
  "host": "127.0.0.1",
  "dialect": "mysql"
}

次のコマンドを叩く
npx sequelize db:create --config config.json

$ npx sequelize db:create --config config.json
Sequelize CLI [Node: 8.12.0, CLI: 5.5.0, ORM: 5.10.0]
Loaded configuration file "config.json".
Database sequelize created.

注意として、npxに続くのがsequelize-cliではなく、sequelizeであること。

これでsequelizeというdatabaseが作成される。

2. tableの作成

main.jsを作成する。

main.js

const { database, username, password, host, dialect } = require("./config");
const Sequelize = require("sequelize");
const { Model } = Sequelize;

// database接続
const sequelize = new Sequelize(database, username, password, {
  host,
  dialect
});

// Model定義
class Project extends Model {}
Project.init(
  {
  title: Sequelize.STRING,
  description: Sequelize.TEXT
  },
  { sequelize, modelName: "project" }
);

(async () => {
  // Projectテーブル作成
  await Project.sync({ force: true })
})()

node main.jsと実行すると 大文字、小文字のsequelizeが出てくるので混乱するかもしれないが、 大文字がSequlizeライブラリとしての情報を、小文字がdatabase接続した情報をもっていると理解すればよいのであろう。

Project.initでORMのMを定義している。

この場合、
- String型のtitle
- Text型のdescription
をもつテーブルを作成しようとしてる。

さらにORMによって自動的に、id(PK)やcreateAt, updateAtも作成される。 createAd, updateAtをつけたくない場合は、timestampsプロパティを利用して、 {sequelize, modelName: 'project', timestamps: false}と記述すればよい。

Project.sync()でtable作成しており、{force: true}を与えることでdrop & createとなる。

insert, update, deleteの実行

以下main.jsの続きとして書いていきます。

1レコードの場合

(async () => {
  
  // instanceを作成する。ここではまだinsertされていない。
  const project = Project.build({
    title: "my awesome project",
    description: "woot woot. this will make me a rich man"
  });
  
  // insert
  const created = await project.save(); // (1)
  // update
  const updated = await project.update({  // (2)
    title: 'update title'
  });
    
  // delete
  const deleted = await project.destroy(); // (3)
})()

帰ってきた値のtoJSON()メソッドを適用すると以下の結果を受け取れる。

(1) insertされたレコードの情報を取得できる(created.toJSON())

{ 
  id: 1,
  title: 'my awesome project',
  description: 'woot woot. this will make me a rich man',
  updatedAt: 2019-07-14T13:44:17.720Z,
  createdAt: 2019-07-14T13:44:17.720Z 
}

(2) updateされたレコードの情報を取得できる(updated.toJSON())

{ 
  id: 1,
  title: 'update title',
  description: 'woot woot. this will make me a rich man',
  updatedAt: 2019-07-14T13:44:17.751Z,
  createdAt: 2019-07-14T13:44:17.720Z 
}

(3) deleteされたレコードの情報を取得できる(deleted.toJSON())

{ 
  id: 1,
  title: 'update title',
  description: 'woot woot. this will make me a rich man',
  updatedAt: 2019-07-14T13:44:17.751Z,
  createdAt: 2019-07-14T13:44:17.720Z 
}

複数レコードの場合

update, destoryは複数レコードに対応できるが、insertの場合、 bulkCreateというメソッドを利用する。

main.js

(async () => {
  const results = await Project.bulkCreate([
    { title: "programming", description: "executing" },
    { title: "reading", description: "executing" },
    { title: "programming", description: "finished" }
  ]);
  console.log(results.map(d => d.toJSON()))
})()
[ 
  { id: 1,
    title: 'programming',
    description: 'executing',
    createdAt: 2019-07-14T13:53:49.986Z,
    updatedAt: 2019-07-14T13:53:49.986Z },
  { id: 2,
    title: 'reading',
    description: 'executing',
    createdAt: 2019-07-14T13:53:49.986Z,
    updatedAt: 2019-07-14T13:53:49.986Z },
  { id: 3,
    title: 'programming',
    description: 'finished',
    createdAt: 2019-07-14T13:53:49.986Z,
    updatedAt: 2019-07-14T13:53:49.986Z } 
]

さいごに

次は、whereやgroup by句やテーブルのjoinなどのselectに関するものをまとめようと思う。 joinこそがORMで一番理解が難しいところだと思ってる。

jsでオブジェクト配列をつかった集合演算

はじめに

jsで集合演算についてしらべてみた。
調べた限り集合演算の例は[1,2,3]と[2,3,4]の積集合をもとめるといったプリミティブ値の配列が多くて、オブジェクト配列をつかった 集合演算が見つからなかったので、残して見ようと思う。

問題(特定の本の返却期日をしらべる)

たとえば、ある人が本屋から複数の本をレンタルしてたときを考えてみる。
レンタルしている本は、id、名前とサイズともつ(A)
本屋はレンタルした本のidと返却期日を保管している(B)

レンタルしてる本からサイズが小さいものの返却期日を調べるにはどうすればよいか?
(A)の部分集合の返却期日を(B)から得たいというわけです。

これらをコードで記述すれば、

// (A) レンタルしてる本一覧
const books = [
  { id: 1, name: 'book1', size: 'large' },
  { id: 2, name: 'book2', size: 'small' },
  { id: 3, name: 'book3', size: 'medium' },
  { id: 4, name: 'book4', size: 'medium' },
  { id: 5, name: 'book5', size: 'small' },
];

// (B) レンタルしている本の返却期日一覧
const lends = [
  { id: 1, end_on: '2019-01-01' },
  { id: 2, end_on: '2019-02-01' },
  { id: 3, end_on: '2019-01-11' },
  { id: 4, end_on: '2019-01-22' },
  { id: 5, end_on: '2019-01-11' },
];

サイズがsmallのものは、idが2,5である。
これの返却期日をlendsから得ると次のとおりになる。

// もとめたいもの(サイズがsmall)
[
  { id: 2, end_on: '2019-02-01' },
  { id: 5, end_on: '2019-01-11' },
]

解決方法

books
  .filter(book => book.size === 'small')  // (1)
  .map(target => lends.find(lend => lend.id === target.id)) // (2)

(1): booksから特定サイズの部分集合をえる

(2): (1)で得た部分集合のidと同じidをもつものをfindで取得してmapで配列を返す

さいごに

jsでオブジェクト配列から積集合?をもとめる書き方を紹介してみました。

使えるときがあるのかな笑
SQLで書くと、

select L.id, L.end_on from books B where size = 'small' left join lends L on B.id = L.id

を求めたいわけでした。

jsで集合演算というとSetという演算子がつかえるみたいですね。
Setをつかって今回の問題はとけるのかな...

flow-typedで型情報を管理しやすくしてみた

flowで型を導入後、毎回importするのが面倒だなと感じるようになってきた。
flow-typedをつかえば型情報をグローバルに定義できるようなので、これでimport文書く手間をカットしたり型情報をうまく管理できるのでは...

ということで今回は、flow-typedをつかって型情報を楽に管理できる方法について書き残そうと思う。

目次

  1. flow, eslintを使えるように準備
  2. flow-typedのインストール
  3. flow-typedディレクトリ作成と.flowconfigの記述
  4. 定義ファイルをつくって動作確認
  5. サードパーティライブラリの型定義を利用してみる

1. flow, eslintの準備

以前作成した記事通りに準備します。 本記事では、割愛させてもらいます。

2. flow-typedのインストール

npm i -D flow-typedを実行する

3. flow-typedディレクトリと設定ファイル

下図のようにrootディレクトリにflow-typedというディレクトリを作成する。
このディレクトリに型定義ファイルを保存していく。
自分で作成したものやreduxなどのサードパーティのライブラリものが保存される。

.
├── .eslintrc.json
├── .flowconfig
├── flow-typed <- つくりました
├── node_modules
├── package-lock.json
├── package.json
└── src

.flowconfig

[ignore]

[include]

[libs]
flow-typed
[lints]

[options]

[strict]

型定義ファイルを保存したディレクトリがどこか指定する。
上記のとおり[libs]に対して、先程作成したflow-typedディレクトリを記述する

4. 型定義ファイルをつくって動作確認

1. 直下にファイルを作成する

flow-typed/sampleType.js

type SampleType = {
  id: number,
  name: string
};

src/sample.js

const obj: SampleType = {
  id: '21',  <- (1)
  name: "SampleName"
};

sample.jsファイル内にimport type {SampleType} from './path'の記述はない
そして、(1) は数値ではないので、エラーとなる。

2. ディレクトリをきってもglobalにロードしてくれる

flow-typed/myModule/myType.js

declare type MyType = {
  uid: number,
  address: string,
  tel: number,
};

これでimport文なしに型チェックしてくれる。

4. サードパーティライブラリの型定義を利用してみる

./node_module/.bin/flow-typed install redux@4
で公開されたreduxの型情報をダウンロードできる。
ファイルは、flow-typedディレクトリに作成される。 あとは、3.の同様に使用できる。

しかし、この場合global空間を汚染させないように作られるため、import文は必要となる。

さいごに

でもflowよりもtypescriptのほうが、人気なんだよね...

参考サイト

flow公式
flow-typed

Prettierについて調査

最近エディタをsublimeからvscodeに変更しました。
その中でPrettierも適用することになったので、こいつのことを理解しようとおもうようになりました。

はじめはimport内のタブサイズが4になってたりと思うどおりに動かなくて断念かと思ったが、再インストールしてようやく想定したとおりに動いてくれたので、安心。

目次

  1. Prettierとはなにか?
  2. eslintと併用する方法
  3. なぜeslint --fixではダメなのか? わざわざprettierを利用する意味はなんだ?
  4. vscodeでprettierを利用する

まずは、npm scripts経由でprettierの挙動を確かめ、その後vscodeで設定する方法を紹介していきたい。

Prettierとはなにか?

Prettierは、ソースをいい感じに整形してくれるツールである。
linterと非常によく似たポジションだと思える。 これを使えばプロジェクト内のコーディングスタイルを統一的にできるわけだ。 自分であっても先週と今週の書き方が異なったりするのでその差をなくしてくれる。

eslintと併用する

早速Prettierをつかってみよう

1. インストール
何はともあれ、必要なものをインストール。 eslintのインストールと設定は、以前の記事をみてほしい。

npm i -D prettier
npm install eslint-config-prettier eslint-plugin-prettier -D

2. eslintの設定にprettierの設定を追加する

.eslintrc.json

{
  "extends": [
    "airbnb",
    "plugin:prettier/recommended" // <- prettierを使うことを宣言
  ],
  "rules": {
    "prettier/prettier": [
    "always",
    // prettierのオプションをここに入れる
    {
      "singleQuote": true,
      "trailingComma": "es5",
      "semi": true,
      "bracketSpacing": true
    }
    ]
  }
}

3. 動作確認 npm scriptsにprettierを登録して、npm run fmtで実行してみる。

修正前

// flow
const obj = {
  key1: 'singleQuote',  // 問題なくシングルウォートを利用してる
  key2: "doubleQuote",  // 誤ってダブルクォートを利用してる
    key3: "tabSize4",   // タブのサイズが4と異なる
  key4: "notComma"      // 最後に,がない
};

const {key1, key2} = obj;  // 中括弧内にスペースがない

// 無駄な改行がある

// 1行が長すぎる関数
function doSomething(argument1: Array<Object>, argument2: boolean, argument3: boolean): string {
  return 'result'   // 最後にセミコロンなし
}

修正後

// flow
const obj = {
  key1: 'singleQuote',
  key2: 'doubleQuote',
  key3: 'tabSize4',
  key4: 'notComma',
};

const { key1, key2 } = obj;

function doSomething(
  argument1: Array<Object>,
  argument2: boolean,
  argument3: boolean
): string {
  return 'result';
}

以上のようにコード上に問題はないが、みやすさ的な保証を担保してくれるわけだ。

なぜeslint --fixではダメなのか? 何が違うのか?

How does it compare to ESLint/TSLint/stylelint, etc.?にあるように、 linterが適用するルールには2つのタイプがあって、
prettierはその一方を楽にするもの。

2タイプというのは、
1. Formatting rules(eg: max-len, keyword-spacing, ...)
2. Code-quality rules(eg: no-unused-vars, no-extra-bind, ...)
のことであり、prettierが扱うのは前者のほうで、
コードの品質的に問題はないが、チーム内でのスタイルルールが異なる問題を自動的に解決してくれるものといわけだ。

たとえば公式の説明をつかえば、次のコードはコード上問題ない。

foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne());

だからlinterで修正はされないが、読みにくさというものはのこる。 これをprettierならば、次のように整形してくれるわけだ。

foo( 
  reallyLongArg(), 
  omgSoManyParameters(), 
  IShouldRefactorThis(), 
  isThereSeriouslyAnotherOne() 
);

vscodeのprettierの設定

1. 拡張機能のインストール
ext install esbenp.prettier-vscode

2. prettier設定反映
設定方法は、以下の通り複数存在するが、今回は3の設定を紹介する. エディタに依存することなく、npm scriptsで確実に動作をするのを確認してから進められ、さらにeslintrcの設定をそのまま活かしたいからである。

  1. 基本設定 > 設定のsetting.jsonに記述
  2. プロジェクトの.prettier.jsonに記述
  3. eslintrc内のprettier設定箇所に記述

3. 保存時にprettier適用
最後に、ファイル保存時にprettierが自動的に実行されるように設定。

settings.json

  ...
  "prettier.eslintIntegration": true,
  "[javascript]": {
    "editor.formatOnSave": true
  },
  ...

以上で完了。あとはいつもどおりに書けばよい。 prettierのoptionについてはまた調べよう。

参考サイト
- Prettier 入門 ~ESLintとの違いを理解して併用する~
- 公式サイト

WebpackのdevServerを使ってもサーバと通信できるフロント開発がしたい

はじめに

自分はReactやReduxを使ってWebアプリを開発するときにWebpackのdevServerにお世話になる。devServerを使えば、ソース変更時に自動でブラウザをリロードしてくれて便利だからである。

しかし、devServerを使った場合、localhostで動かしてるwebサーバと通信できない。たとえば、devServerは、http://localhost:8080で起動する。
そこから、http://localhost:8000で動作するwebサーバとHTTP通信することは異なるオリジンにアクセスすることになってしまい通信は失敗する。
localhost:8080とlocalhost:8000は、異なるオリジンなので前者から後者に対して通信することは普通できないのである。

ローカルでwebサーバを動かす一方でフロント開発でdevServerを使ってるとき、フロントがサーバからデータ取得できるようにするにはどうすればよいのだろうかと壁にぶつかった。異なるオリジンであっても通信できるようにするにはどうすればよいのだろうかということである。

なので今回は、異なるオリジンであってもサーバと通信できる設定について備忘録を残そうと思う。

目次

  1. expressで簡単なwebサーバの準備
  2. 同一オリジンから通信できることの確認
  3. 別オリジンから通信の失敗確認
  4. expressのCORS設定
  5. SessionありCORS

1. expressで簡単なwebサーバの準備

nodejsのexpressを使用する。
expressでのwebサーバ構築方法は、割愛させていただきます。

npm i -S expressでexpressをインストール
次のapp.jsを用意する。

app.js

const express = require('express');
const app = express();

// "/"にアクセスしたときにres.jsonの引数値を返す
app.get('/', (req, res) => {
  res.json({
    result: 'success',
    message: 'hello express',
  });
});

// 8000ポートで起動
const server = app.listen(8000, () => {
  console.log(`Node.js is listening to PORT: ${server.address().port}`);
});

あとは、node app.js(あるいはnpm scriptsに記述してnpm start)でサーバを起動させる。すると、http://localhost:8000にアクセスしたときにres.json()の引数に指定したjsonを受け取ることができる。

これでまずサーバサイドの実装は終了。

2. 同一オリジンから通信できることの確認

ブラウザを起動させ、urlにhttp://localhost:8000と入力すると、 先程のjsonが画面に表示される。

さらに、fetchメソッドも使って取得できることも確認しよう。
console画面に次を入力して取得できることを確認する。

fetch('/', {
  method: 'GET',
  headers: { 'content-type': 'appliaction/json' }
}).then(r => r.json()).then(r => { console.log(r); })

// コンソール出力値
// {result: "success", message: "hello express"}

3. 別オリジンから通信の失敗確認

グーグルトップページ(https://wwww.google.co.jp)を開いて、同じくコンソール画面に次を入力する

// ドメインが違うので、URLを省略せずに書く
fetch('http://localhost:8000', {
  method: 'GET',
  headers: { 'content-type': 'appliaction/json'}
}).then(r => r.json()).then(r => { console.log(r); })

fetchの第一引数は、http://localhost:8000になる。
"/"は、https://www.google.co.jpを意味するからだ。

その結果は次のとおりである。

f:id:poppon555:20181008204419p:plain

やはり、ドメインが異なるため通信失敗する。 エラー内容にある通り、HTTPリクエストのヘッダーに'Access-Control-Allow-Origin'が存在しないからダメだよと言われるている。(他にもヘッダーに足りない情報がある)

4. expressのCORS設定

異なるオリジンからの通信も許可するために、サーバサイドに変更を加える。

app.js

const express = require('express');
const app = express();

// ------------- ここから --------------
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', "*"); // (1)
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); // (2)
  res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS'); // (3)
  next();
});
// ------------- ここまで --------------

app.get('/', function(req, res) {
  res.json({
    result: 'success',
    message: 'hello express',
  });
});

const server = app.listen(8000, () => {
  console.log(`Node.js is listening to PORT: ${server.address().port}`);
});

追加した内容については、
(1): どんなオリジンからの通信を許可("*"はすべてを意味する)
(2): 指定するheader情報の通信を許可
  今回fetchメソッドでは、headersに'content-type': 'appliaction/json'を指定してるため
(3): GETからOPTIONSまでのHTTPメソッドを使って通信すること許可
ということになる。

これでもういちど、fetchメソッドを実行すると取得できるようになる(やったね)。

5. SessionありCORS(おまけ)

session情報をもたせるときは、もう少し改良が必要になる。 変更箇所のみ記述する

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin); // (1)
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Credentials', true); // (2)
  next();
});

(1): "*"ではなく、リクエストからオリジン情報を取得し、そのからの通信を許可
(2): sessionを使うときはこれを追加してあげる
これでsessionありの通信も可能となるが、最後にフロントでのfetchメソッドも確認しておく。

fetch('http://localhost:8000', {
      method: 'GET',
      headers: { 'content-type': 'application/json' },
      credentials: 'include', // (3)
    }).then(r => r.json()).then(r => { console.log(r); })

(3)のとおり、credentailをfetchメソッドのオプションに追加する。

最後に

これでサーバと通信できるフロント開発ができるように助けになってくれれば幸いである。非常に駆け足で説明してきたので、かなり読みにくい文章になってしまった。申し訳ない(あとでもう少し読みやすくしようと思います。)

参考サイト

http://var.blog.jp/archives/60055220.html https://qiita.com/MuuKojima/items/2b2e7bc0db8d5e97ada9 https://blog.kazu69.net/2017/03/23/http-request-using-cors/