- 投稿日:2020-04-01T20:14:17+09:00
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-devFROM 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-devFROM 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.ymlversion: "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で某とか、そういうあれでなんかやっていきます。
- 投稿日:2020-04-01T18:58:22+09:00
途中で戻したり進めたりできるイテレータ
構文解析する際に、文字列イテレート中に戻したりスキップしたりできるイテレータがあれば便利かと思い作ってみました。
参考: https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Iterators_and_Generatorsfunction 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 !
- 投稿日:2020-04-01T18:13:10+09:00
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 の下に認識しているアプリケーションが表示されます
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
- 投稿日:2020-04-01T17:19:50+09:00
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.js
のformatDate()
- リポジトリでいうとどこかは、軽く見たけどわからなかった
- 日付時刻StringをDate型に変換して読み出す処理
- TZ環境変数にもとづいて、入力を補正していない
- 結果、読み出される時刻がズレる(JSTなら-9時間されてる)
- ソースで具体的にどこに該当するかは、ざっくりデバッグしたけどわからなかった。
query()
中のresult補正かけてるあたりのどこかでやるべき、なのかも。結果、JST環境からの読み書きだけ見ると、同じだけズレるので 書き込み前=読み込み前 となっていますが、
CreatedDateColumn() 等でDB側で生成された時刻を読みだした場合ズレたり、
そもそもDB直接覗くとUTCのくせにJST扱いで記録されてたりと、
いろいろ気持ち悪い現象が発生してしまっていました。(補足)原因はわかってるけどコントリビュートしたくない
ざっくり調べた結果、上記原因が解消されれば治るは治るはずなんだけど、
以下の背景からコストが高くなりそうなのでしてません。
issueくらいは投稿してあげてもいいかも。手が空いたらやる。
- それがTypeORM由来なのかプラグイン由来なのか正直わかりきらない
- TypeORMのプラグインにコントリビュートするにはTypeORMの知識も必要である
- プラグインの更新が活発でない(8ヶ月前とか)
対策
@Column()
の option である transformer を使い、
書く直前と読んだ直後に自力で時刻を補正します。
Entity インスタンス自体の時刻には影響しないよう気を使っています。orm-opts.tsimport 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.tsexport 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に補正されて読まれるようになります。
- 投稿日:2020-04-01T12:45:18+09:00
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.jsmodule.exports = { [...] modulePathIgnorePatterns: ["<rootDir>/scripts/"], [...]
- 投稿日:2020-04-01T09:46:41+09:00
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通りに分けられるんじゃないでしょうか。
- プロジェクトルートにある
babel
の設定ファイル(.babelrc
babel.config.js
)をbabel
が自動的に読み込むwebpack
から実行環境によって手動(設定ファイルのコード上)で呼び出す設定を変える上記2パターンそれぞれについて説明をしていきます。
3.1. 設定ファイルを自動的に読み込む場合
この場合、
babel.config.ts
は次のように書くことができます。babel.config.tsimport { TransformOptions, ConfigAPI } from '@babel/core' export default function (api: ConfigAPI): TransformOptions { return { presets: [ '@babel/preset-env', ], } }
TransformOptions
とConfigAPI
という見慣れないものが出てきましたが、TransformOptions
はお馴染みの babelの設定ファイルの内容 、ConfigAPI
は babelの設定API のことです。
これを.ts
ではなく.js
で書くと次のようになります(ドキュメントにほぼ同じものが載っていますが)。babel.config.jsmodule.exports = function(api) { return { presets: [ '@babel/preset-env', ], }; }ここまでくればあとは
babel.config.ts
をbabel
に読み込ませるだけですが、babel
はwebpack
のように.ts
の設定ファイルをいい感じに解釈してくれません。webpack.config.ts// ... module: { rules: [ { test: /\.[jt]sx?$/, loader: 'babel-loader', exclude: /node_modules/, options: { configFile: 'babel.config.ts' }, }, // ...このように書いたとしても
babel
はbable.config.ts
をJSONとして読み込もうとするので構文エラーになります。
そこでtsc
コマンドで.js
ファイルにトランスパイルする必要があります。npx tsc babel.config.tsこれで
babel.config.js
が生成されるので設定ファイルを読み込むことができますが、いちいちトランスパイルするのは面倒ですし、babel.config.ts
とbabel.config.js
の2ファイルが並ぶのはあまり気持ちのいいものではないですね。3.2. webpackから実行環境によって設定を変える場合
3.1. の方法だとあまりすっきりできないので、自分はこちらの方法を採用しています。
特にwebpack4
ではwebpack-merge
を使って呼び出す設定ファイルを切り替えるのがメジャーらしい(?)のでこちらの方が合っている気がします。babel.config.tsimport { 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.tsimport { 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 devConfigwebpack.config.prod.tsimport { 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
で記述することでタイポなども減らせていいんじゃないでしょうか。
もっといい方法などあれば教えて頂けるとうれしくなります。