20200401のNode.jsに関する記事は6件です。

MERNスタックをDockerでやっていく

はじめに

30代正社員経験のないクズ人間だけどSHUSHOKUできました!やったー!
うれC!!!!!!!!うれC!!!
MERNスタック使って、ポートフォリオ用にWebアプリ作ってみたけど、大変だった気がします。正直全部大変だし難しいし自分が何やってるかよくわからない感じになります。頭が悪いので整理ができない……(´・ω・`)
DockerでMERNスタック環境構築したらDocker使っちゃう気持ちもアゲアゲでいけるのでは?などと考え、あとからECRやら楽なんじゃね?とか思っていた時期が自分にもありました。

MERNスタックでなんかDockerもつかって気軽に色々できればいいなあというわけで、今後忘れないように書いておこうと思います。

Dockerについては、「何故Dockerなのか」とか、なんか公式読めば良いとかいう風潮がある気がします。
公式は字が多すぎて読む気がおきないみたいなやつはあります。でも、読め!とインターネッツの中の人たちが言ってくるので読みました。読んでもよくわからないので、脳がやばい。
脳みそやわやわな状態でやっているので、やっていける気がしない。

チラシの裏にでも書いてろ!という記事です。でもQiitaに書いたろと思いました。承認欲求モンスターなので。謝罪しておきます。申し訳ございません。

フォルダ構成

とりあえず、こんなん

.
├── README.md
├── backend
│   ├── Dockerfile-dev
│   ├── app.js
│   ├── config.env
│   ├── controllers
│   ├── middleware
│   ├── models
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   ├── routes
│   ├── uploads
│   └── utils
├── frontend
│   ├── Dockerfile-dev
│   ├── build
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   └── src
├── dev-docker-compose.yml
├── node_modules
├── package-lock.json
└── package.json

メインにフロントエンドとバックエンドそれぞれフォルダを作りその中にdev環境Dockerfileをそれぞれ置いておく。
メインフォルダにはdev環境Docker-compose.ymlをつくる。
とりあえずsudo apt install docker-composeやら色々はやってる状態。
他のフォルダなんかは、皆自由にやっていっていると思います。

基本的に↓

nanka_app_nandemo_yoi_example/
├── node-backend/
│   └── Dockerfile
├── react-frontend/
│   └── Dockerfile
└── docker-compose.yml

こういう構成で作っていきたい気持ち。

バックエンド(MERNのN担当Node.js)

とりあえず動かしたいので、こんな感じに。

Dockerfile-dev
FROM node:10.18.0-alpine3.9

EXPOSE 3000

RUN apk add --update curl
WORKDIR /usr/src/app

COPY package.json package-lock.json* ./

RUN npm cache clean --force && npm install && npm install -g nodemon
COPY . .

CMD ["npm", "run", "dev"]

自分にはdev環境とproduction環境をしっかり構成したいという夢がありました
というわけで、devになっていますが、別にDockerfileとしていれば良い。

フロントエンド(MERNのR担当React.js)

担当とか書くと、なんかMERNの中で誰推し?みたいな雰囲気になっていいですね。
皆JS界からきてるわけですし、でかいアイドルグループみたいなもんやろ。しらんけど。
自分は誰推しでもないです、皆むずかしい。もう誰も信じない。

とりあえずこんな感じ

Dockerfile-dev
FROM node:10.18.0-alpine3.9

EXPOSE 3000

RUN apk add --update curl
WORKDIR /usr/src/app

COPY package.json package-lock.json* ./

COPY . .

CMD [ "npm", "start" ]

ほぼバックエンド側と変わらない感じのやつ。少しでも、行数を減らしたい。行が増えると絶望感が増す。

COMPOSEファイル(MERNのM担当Mongoあたりも登場)

コンポーズファイル作ります。
こいつがないと私にはなにもできない。

dev-docker-compose.yml
version: "2.2"

services:
  mongodb:
    image: "mongo"
    ports:
      - "27017:27017"
  backend:
    build:
      context: ./backend/
      dockerfile: Dockerfile-dev
    ports:
      - "6200:6200"
    volumes:
      - ./backend:/usr/src/app
    depends_on:
      - mongodb
  frontend:
    build:
      context: ./frontend/
      dockerfile: Dockerfile-dev
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/usr/src/app
    depends_on:
      - backend

ビルドしたい

docker-compose -f dev-docker-compose.yml buildこれでビルドできる。
あとは毎回docker-compose -f dev-docker-compose.yml upとかすれば良い。

これで、なんか構築はされる感じになっている。
あとはCircleCIがどうとか、Node.js側でAPIがどうのとかReact側でSPAがどうとか、MongoDBでAtlasで某とか、そういうあれでなんかやっていきます。

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

途中で戻したり進めたりできるイテレータ

構文解析する際に、文字列イテレート中に戻したりスキップしたりできるイテレータがあれば便利かと思い作ってみました。
参考: https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Iterators_and_Generators

function iter(pattern) {
    let index = 0
    return {
        [Symbol.iterator]: function() {
            return this
        },
        next: function() {
            if (index < pattern.length) {
                return { value: pattern[index++], done: false }
            } else {
                return { done: true }
            }
        },
        fetch: function() {
            return pattern[index]
        },
        back: function() {
            index > 0 && index--
        },
        skip: function() {
            index++
        },
    }
}

実際に利用したコードは長くなってしまったので、ちょっと無理矢理な使用例を示します。

使用例
let it = iter("Helo!!!!!, world!")
for (let c of it) {
    console.log(c)                    // 'Hel'
    if (c === 'l') break
}
it.back()                             // back to 'l'
console.log(it.next().value)          // 'l'
console.log(it.next().value)          // 'o'
while (it.fetch() === '!') it.skip()  // skip '!'
for (let c of it) {
    console.log(c)                    // ', world!'
}
実行結果
H
e
l
l
o
,

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

Node.jsをChrome DevToolsでデバッグする方法

Node.js は標準で Chrome DevTools と接続しデバッグする機能が付いています
普段から使っている Chrome DevTools と同じ感覚で使えます
ヒープメモリのスナップショットも取れるためメモリリーク調査にとても便利だったのでやり方をまとめておきます

対応バージョン

  • node v6.3.0以上

やり方

1. node を --inspect オプションをつけて起動する

node --inspect index.js

2. Chrome で chrome://inspect を開く

こんな画面が開きます
うまく動いていると Remote Target の下に認識しているアプリケーションが表示されます
スクリーンショット 2020-04-01 17.59.04.png

3. Open dedicated DevTools for Node をクリック

いつもの Chrome DevTools が開きます

4. デバッグする

ヒープメモリのスナップショットをとったり色々できます

参考

https://nodejs.org/en/docs/guides/debugging-getting-started/
https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27

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

TypeORM x AuroraDataAPI - タイムゾーン問題への対処

TypeORM x AuroraDataAPI - TIMEZONE問題への対処

ローカルタイムゾーンがJSTだとしたとき、
DBに書き込まれる時刻が9時間未来になる(JSTのまま書かれてしまう)
DBから読み込まれる時刻が9時間過去になる(JST扱いで読まれてくる)問題への対処です。

前置き

本記事の解決法では、TypeORMの外側で自力で時刻を補正するため、
ライブラリ(TypeORMとAuroraプラグイン)の仕様が変わったり、バグが治った場合は毒になりえます。
そのへんの影響まで自分で管理してやんよシュババってひとむけです。

環境

  • Aurora Serverless MySQL 5.6
    • time_zone, system_time_zone: UTC
  • TypeORM 0.2.24
  • typeorm-aurora-data-api-driver 1.1.8
  • Node 10
    • timezone(TZ環境変数): Asia/Tokyo

上記環境 (TypeORM x AuroraDataAPI) での観測ですが、
DBが違う場合でも同じ症状には同じ対策が適用可能なはずです。

原因

おそらく、 typeorm-aurora-data-api-driver に関する、以下のバグです。

  • Data型を日付時刻Stringに変換して書き込む処理
    • TZ環境変数にもとづいて、出力を補正していない
    • 結果、書き込まれる時刻がズレる(JSTなら+9時間されてる)
    • 配布物でいうと、 typeorm-aurora-data-api-driver.umd.jsformatDate()
      • リポジトリでいうとどこかは、軽く見たけどわからなかった
  • 日付時刻StringをDate型に変換して読み出す処理
    • TZ環境変数にもとづいて、入力を補正していない
    • 結果、読み出される時刻がズレる(JSTなら-9時間されてる)
    • ソースで具体的にどこに該当するかは、ざっくりデバッグしたけどわからなかった。
      • query() 中のresult補正かけてるあたりのどこかでやるべき、なのかも。

結果、JST環境からの読み書きだけ見ると、同じだけズレるので 書き込み前=読み込み前 となっていますが、
CreatedDateColumn() 等でDB側で生成された時刻を読みだした場合ズレたり、
そもそもDB直接覗くとUTCのくせにJST扱いで記録されてたりと、
いろいろ気持ち悪い現象が発生してしまっていました。

(補足)原因はわかってるけどコントリビュートしたくない

ざっくり調べた結果、上記原因が解消されれば治るは治るはずなんだけど、
以下の背景からコストが高くなりそうなのでしてません。
issueくらいは投稿してあげてもいいかも。手が空いたらやる。

  • それがTypeORM由来なのかプラグイン由来なのか正直わかりきらない
  • TypeORMのプラグインにコントリビュートするにはTypeORMの知識も必要である
  • プラグインの更新が活発でない(8ヶ月前とか)

対策

@Column() の option である transformer を使い、
書く直前と読んだ直後に自力で時刻を補正します。
Entity インスタンス自体の時刻には影響しないよう気を使っています。

orm-opts.ts
import moment, { Moment } from "moment-timezone";
import { ColumnOptions, EntityMetadata, EntitySchema } from "typeorm";

/**
 * Date型のoffsetに基づいて読み書きされているので
 */
const TYPEORM_LOCAL_OFFSET = new Date().getTimezoneOffset();
/**
 * DBのタイムゾーンは基本UTCの前提とする。
 * 異なる場合は、setCorrectOffset()で任意の補正時間をセットする。
 */
const DB_OFFSET = moment.tz("UTC").utcOffset();

export class OrmOpts {
  /**
   * TypeORM ごしにDatetime型を書く直前、読んだ直後に、この分数だけ時刻をずらす。
   * DB時刻がUTCにも関わらず、JSTで(+09:00のまま)書き込まれたりする問題への対処のため。
   *
   * 初期値は 0(UTC) - TZ環境変数のoffset(JSTなら540)
   */
  static CORRECT_OFFSET = DB_OFFSET - TYPEORM_LOCAL_OFFSET;

  static MOMENT: ColumnOptions = {
    type: "datetime",
    transformer: {
      from: (from: Date) => {
        if (!from) return from;
        const fromM = moment(from).add({
          minutes: OrmOpts.CORRECT_OFFSET
        });
        return fromM;
      },
      to: (to: Moment) => {
        if (!to) return to;
        const toD = to
          .clone()
          .subtract({ minutes: OrmOpts.CORRECT_OFFSET })
          .toDate();
        return toD;
      }
    }
  };

  static DATE: ColumnOptions = {
    type: "datetime",
    transformer: {
      from: (from: Date) => {
        if (!from) return from;
        const fromM = (OrmOpts.MOMENT.transformer as any).from(from);
        return fromM.toDate();
      },
      to: (to: Date) => {
        if (!to) return to;
        to = (OrmOpts.MOMENT.transformer as any).to(moment(to));
        return to;
      }
    }
  };
}
types.ts
export class TestEntity extends BaseEntity {
  @PrimaryGeneratedColumn("increment") seq: number;
  @CreateDateColumn(OrmOpts.DATE) createdAt: Date;
  @CreateDateColumn(OrmOpts.MOMENT) createdAtM: Moment;
  @Column(OrmOpts.DATE) dateAt: Date;
  @Column(OrmOpts.MOMENT) dateAtM: Moment;
}

結果

これで、ローカルのTIMEZONEに関わらず、
正しい時刻(このコードではUTC)でDBに書き込まれるようになります。
読み出すときはローカルTIMEZONEに補正されて読まれるようになります。

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

SyntaxError: Identifier 'jest' has already been declared

create-react-appをejectしてyarn testを実行すると掲題のエラーが発生しました。
たまたま直せたので、直し方を書いておきます。

 FAIL  scripts/test.js
  ● Test suite failed to run

    SyntaxError: Identifier 'jest' has already been declared

      at Runtime._execModule (node_modules/jest-runtime/build/index.js:867:68)

jest がすでに定義されているよ、というエラーです。

このエラーだけだと何が起きているかよくわからなかったんですが、 https://github.com/facebook/create-react-app/issues/1319 を読むと、scripts/test.js もjestのテスト対象になっているせいで発生しているようでした。

jestの対象ディレクトリからscriptsを除外すれば直ります。

jest.config.js
module.exports = {
[...]
  modulePathIgnorePatterns: ["<rootDir>/scripts/"],
[...]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

babelの設定ファイルをTypeScriptで書く

表題の通りです。
babel の設定ファイルといえば .babelrc babel.config.js での記述が一般的だと思います。これを babel.config.ts に記述できるようにします。

1. 必要モジュールのインストール

とりあえず webpack 前提で必要最低限なものだけインストールします。

npm i -D webpack webpack-cli typescript ts-node @babel/core @types/babel__core babel-loader @babel/preset-env

ほとんど説明するほどのものではないですが、重要なのは @types/babel__core です。
ここに設定ファイルで使用する型情報が載っています。

2. TypeScriptの設定

tsconfig.json を書きます。

tsconfig.json
{
  "ts-node": {
    "compilerOptions": {
      "module": "commonjs",
      "target": "es5",
    },
  },
}

ここでは ts-node の設定だけ行います。

3. babelの設定

ここからが本題です。 babel.config.ts を書きますが、 設定の読み込み方によって書き方も変わってきます。
だいたい次の2通りに分けられるんじゃないでしょうか。

  1. プロジェクトルートにある babel の設定ファイル( .babelrc babel.config.js )を babel が自動的に読み込む
  2. webpack から実行環境によって手動(設定ファイルのコード上)で呼び出す設定を変える

上記2パターンそれぞれについて説明をしていきます。

3.1. 設定ファイルを自動的に読み込む場合

この場合、 babel.config.ts は次のように書くことができます。

babel.config.ts
import { TransformOptions, ConfigAPI } from '@babel/core'

export default function (api: ConfigAPI): TransformOptions {
  return {
    presets: [
      '@babel/preset-env',
    ],
  }
}

TransformOptionsConfigAPI という見慣れないものが出てきましたが、 TransformOptions はお馴染みの babelの設定ファイルの内容ConfigAPIbabelの設定API のことです。
これを .ts ではなく .js で書くと次のようになります(ドキュメントにほぼ同じものが載っていますが)。

babel.config.js
module.exports = function(api) {
  return {
    presets: [
      '@babel/preset-env',
    ],
  };
}

ここまでくればあとは babel.config.tsbabel に読み込ませるだけですが、 babelwebpack のように .ts の設定ファイルをいい感じに解釈してくれません。

webpack.config.ts
// ...
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          configFile: 'babel.config.ts'
        },
      },
// ...

このように書いたとしても babelbable.config.ts をJSONとして読み込もうとするので構文エラーになります。
そこで tsc コマンドで .js ファイルにトランスパイルする必要があります。

npx tsc babel.config.ts

これで babel.config.js が生成されるので設定ファイルを読み込むことができますが、いちいちトランスパイルするのは面倒ですし、 babel.config.tsbabel.config.js の2ファイルが並ぶのはあまり気持ちのいいものではないですね。

3.2. webpackから実行環境によって設定を変える場合

3.1. の方法だとあまりすっきりできないので、自分はこちらの方法を採用しています。
特に webpack4 では webpack-merge を使って呼び出す設定ファイルを切り替えるのがメジャーらしい(?)のでこちらの方が合っている気がします。

babel.config.ts
import { TransformOptions } from '@babel/core'

// 開発環境用の設定
export const dev: TransformOptions = {
  presets: [
    '@babel/preset-env',
  ],
  sourceMaps: true,
}

// 本番環境用の設定
export const prod: TransformOptions = {
  presets: [
    '@babel/preset-env',
  ],
}
webpack.config.dev.ts
import { Configuration } from 'webpack'
import merge from 'webpack-merge'
import config from './webpack.config.common'
import { dev as devBabelConfig } from './babel.config'

const devConfig: Configuration = merge(config, {
  mode: 'development',
  devtool: 'source-map',
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: devBabelConfig,
      },
  // ...
}

export default devConfig
webpack.config.prod.ts
import { Configuration } from 'webpack'
import merge from 'webpack-merge'
import config from './webpack.config.common'
import { prod as prodBabelConfig } from './babel.config'

const prodConfig: Configuration = merge(config, {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: prodBabelConfig,
      },
  // ...
}

export default prodConfig

おわりに

以上、 babel の設定ファイルを TypeScript で書いてみました。
TypeScript で記述することでタイポなども減らせていいんじゃないでしょうか。
もっといい方法などあれば教えて頂けるとうれしくなります。

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