はらへり日記

腹に弾丸

Webpackで複数のファイルをそのままバンドルする

やりたいこと

お仕事でページごとに必要なモジュールのみimportしたJSを実装し、それぞれコンパイルしたいという場面があった。

イメージ的にはsrc/(pc|sp)/**/*.jsをフォルダ構成やファイル名をそのままpublic/js配下に吐きだすといった感じ。

Webpackは基本的に複数ファイルをよしなに1ファイルにバンドルするものなので結構情報が少なくて詰まったのでメモっておく。

実装方針

Webpackでは複数ファイルをコンパイル対象として、それぞれ特定フォルダに吐き出すといったことをサポートしている。

multiple entry points

公式からコードを引用するとこんな感じ。

{
    entry: {
        a: "./a",
        b: "./b",
        c: ["./c", "./d"]
    },
    output: {
        path: path.join(__dirname, "dist"),
        filename: "[name].entry.js"
    }
}

こんな感じで頑張ってentryに書いていってもいいんだがこれだと2つ問題がある。

  • ファイルが増えれば増えるほど自分で書き足していかなきゃいけない
  • entryのkey名でファイルが吐きだされるのですべてoutputで定義されたフォルダに吐き出される

2つ目の問題この記事がわかりやすく説明してくれてる。

webdesign-dackel.com

この記事ではファイルがそこまで多くない想定なので手でentryの項目を書いているが、私のケースだとファイルが大量にあったので以下の方針で設定を試みた。

  • globを使ってコンパイル対象のファイルを全て取得する
  • globで取った配列からoutputを基準として出力してほしいフォルダ + ファイル名をkey, コンパイル対象のファイルパスをvalueとした連想配列を作る
  • それをentryに渡す

コード

要するにこんな感じです。

この例はsrc/pc/*.jssrc/sp/*.jspublic/js/配下にフォルダ構成そのままに出力する例です。

import webpack from 'webpack';
import path    from 'path';
import glob    from 'glob';

const jsBasePath = path.resolve(__dirname, 'src/');
const jsCompileFolders = ['pc', 'sp'];

const targets = glob.sync(`${jsBasePath}/+(${jsCompileFolders.join('|')})/*.js`);
const entries = {};
targets.forEach(value => {
  const re = new RegExp(`${jsBasePath}/`);
  const key = value.replace(re, '');
  entries[key] = value;
});

export default {
  entry: entries,
  output: {
    path: path.join(__dirname, 'public/js'),
    filename: '[name]',
  },
}

色々省略してますが、まずはコンパイル対象のフォルダを取る部分。

const targets = glob.sync(`${jsBasePath}/+(${jsCompileFolders.join('|')})/*.js`);

ここで同期処理でコンパイルしたいJSファイルの一覧を取ってきてます。 これを{[outputを基準として吐き出したいフォルダ+ファイル名]: [コンパイルするファイル]}となるkey-valueをここで作ります。

const entries = {};
targets.forEach(value => {
  const re = new RegExp(`${jsBasePath}/`);
  const key = value.replace(re, '');
  entries[key] = value;
});

もっといい方法ありそうなんですが、愚直にkey-valueをオブジェクトに突っ込んでるだけです。 あとはこれをwebpackのconfigに渡して終わり。

サンプルコード

超必要最低限書いたサンプルコード置いておきます。

github.com

まとめ

webpack楽しいぞい

webpackのDefinePluginとbabel-plugin-transform-environment-variablesの併用には注意

やりたいこと

webpackを使ってフロント用のファイルをバンドルしたい。

そのとき、環境変数に合わせて値を変えたいという場面があった。

ノリ的には以下のようなコード。

if (process.env.NODE_ENV === 'development') {
  console.log('Debug message');
}

この例だと開発か本番かの判定だが、他にもステージングの時はこのAPIのエンドポイント、といったことがしたかった。

ハマった

webpack初心者だったのでこの記事を参考に書いてみた。

細かいところは違うけどほぼ内容一緒なのでコードは割愛。

geta6.hatenablog.com

そしてコンパイルされたコードを見るために以下のように適当にコード書いて動かしてみる。

console.log(process.env.NODE_ENV);

そしてビルドし、吐き出されたコードを見てみる。

npm run buildpackage.json"build": "webpack"とだけ定義してある。

npm run build
node dist/index.js
> undefined

なんで!!!!!!

原因

原因はなんてことなくて、これを併用していたせいだった。

babeljs.io

こいつはBabelでコンパイルする際、環境変数を静的に置き換えてくれる。

WebpackでBundleするとき、内部的な処理順は

という順序になっている。

私はDefinePluginとtransform-inline-environment-variablesを併用、かつビルド実行時に環境変数をexportしていなかったので

  • transform-inline-environment-variablesがprocess.env.HOGEを静的に置き換え(exportされてないのでundefined)
  • DefinePluginが実行(でもprocess.env.HOGEは全部置換済みなので何も起きない)

という感じになっていた。

なのでやり方的には

  • webpack実行時に環境変数にすべてexport & transform-inline-environment-variables
  • transform-inline-environment-variablesは使わずDefinePluginにすべてkey-valueで渡す

の2通り。どっちがいいかは好みや場合によりけりかな…

まとめ

圧縮の道はツライ。

ISUCON6予選に出た

結果

正確なスコアはメモし忘れたんですが、最高点が15000点ぐらいでした。

後半はずっと14000点を前後してた

何をしたか

自分用に覚えてる範囲で軽くメモ。

  • まずalp, pt-query-digestを仕込んでベンチを叩く。スコア200くらい
  • DB周りの@aboyとミドルウェア、OSの@ktarow、アプリの自分で各々予め決めてたことをやる
  • アプリを最初に触ってみる。はてな感すごい
  • DBのデータ量が少ないこと、データ数もテーブルも少ないことに戸惑う
  • 2つWebサーバが立ってることにとまどう
  • とりあえずインデックスを貼ったりnginx周りをチューニングしてもらったりDBにメモリ振ったり
  • 2つのWebサーバ間でお互いを叩いていて無駄だったので消す

ここまででちゃんとアプリケーションコードを読み切れていなかったり、作戦会議の詰めが甘くてFAIL地獄にハマる。

  • 昼頃にFAIL地獄を抜けて11000点くらいになる
  • この時点で去年のスコア(一概に比べられないけど)の10倍以上だったのでハイタッチする
  • ここからインフラのやることがあまりなくなってくる

メモリを食わせてもスループットがあがらなかったり、はちゃめちゃに遅いクエリが無くてここらへんでやっときちんとアプリコードを精査する。おそすぎた。

  • entry本文を表示するために毎回7000語で正規表現をかけていることに気づく
  • ハッシュへの置換とハッシュからリンクへの置換を一挙にやろうとしたがやってみて失敗したことに気づく

例えば1ってkeywordがあると、<a href="/keyword/%10"></a>みたいなリンクがあった時にリンクの内容を置換してしまう。

よく考えればわかることだった…

  • ここらへんからこまめにRedisにデータを突っ込んでいく
  • 突っ込んだ後、それを利用できるものは利用するよう書き換えていく
  • なんやかんやでちょっとずつスコアがあがり15000点になってから2時間ぐらいエラーと戦う

という感じでした。超雑。

なんか知らないけど平尾山のページでめっちゃエラー指摘されました。もうしばらく平尾山は見たくない

所感

木曜から休暇取ってたのでずっとISUCON5の予選で練習してたから頭がDB脳だったのがあまりよくなかった。

始まってみるとDBで改善できるところが少なすぎて結構戸惑った。

去年の反省生かして最初の30分くらいは何もせずにアプリコード読んでたけど、1時間半くらいかけてもよかったかも。

きちんと読み切った上でガッツリ作戦立てたほうがよかった。全部Redisにのっけるとか。

あと、終わって冷静になってから気づいたけど

  • htmlifyの内容を全てキャッシュ
  • POST /keywordが来たらDBからそのキーワードを含むものだけ探してキャッシュを作り直す(Twitterでやったって人見た)
  • ただ、リクエストの中でキャッシュ作り直すとレスポンス遅すぎて減点が痛いのであらかじめPHPでデーモン立ててそこに食わせるようにすればよかった

って思った。ただ、デーモン立てるっていってもそのスクリプト用意してなかったし今度から用意しようと思った。

ループ見たときはPHP並列できない…つらい…ってなってた。

トラブル

東新宿コワーキングスペースでやったんだけど、途中で充電器抜いたらスパークして死ぬかと思った。

f:id:sota1235:20160919013246j:plain

すごい音なったしISUCON中に死ぬとこだった。まじで。

反省会

盛大に反省した。

f:id:sota1235:20160919013356j:plain

悔しいポイントは本当に多くて、予選突破まで行かなくても人権が得られる得点には届きたい人生だった。

ただ、去年は本当に無知で何もできなかったけど今年は終始頭を使えてたので成長を感じた。

何よりスコアが15倍ですからね!!!!!!!!!!!

来年もきっとあると信じて引き続き精進したい。

まとめ

エンジニアになるために人権勝ち取っていこうな。

npm scriptsでエラーログを表示させたくない話

npm run hogeでエラーを出したくない

eslintでのチェックやトランスパイルの実行は下のような感じでpackage.jsonに書いてnpm run lint等で実行するようにしてる。

{
  "scripts": [
    "lint": "eslint src/",
    "build": "babel src --out-dir dest"
  }
}

その際、eslint実行等の場合はコマンドの実行自体がコケるだけでnpmが鬱陶しいエラーを出してくる。

npm run lint実行結果

欲しいのはnpm scriptの結果で、下のはnpm初心者には無益で紛らわしいログでしかない

これをどうにかしたい

解決策を調べる

どうにかできないか調べてみた。

解決策その1:--silentオプションをつける

npmにはloglevelという概念があり、これにオプションを指定するとログの出力形式を変更できる。

docs.npmjs.com

その中の1つに--silentがあるのでnpm scriptを実行する際にこのオプションをつける。

省略形で-sでもよい。

--slientオプションつきnpm run lint

これで鬱陶しさはなくなったが

  • 毎回オプションをつけなければいけない
  • npm-debug.logを吐き出さないので本当にnpm由来のエラーが発生したらオプション外してもう一回実行しなきゃいけない

というデメリットがある

解決策その2:.npmrcを設定する

最近こんな記事を読んだ。

qiita.com

今まで知らなかったのが本当にもったいないくらい最高の話で、ローカルにnpmコマンドの設定を保持できる。

なので.npmrcを作成し、以下の用なオプションを指定すると常時、loglevelsilentになる

loglevel=silent

これで毎回オプションを足す手間は省ける。

ただし、これにもデメリットがあって

  • CI等でこうするとエラーが発生したときにログが読めない
  • npm-debug.logが吐き出されない

という問題が依然として残る

解決策その3:aliasでごまかす

後にも言うけどこの問題に根本的解決策は現状ないです。

なので上記デメリットを吸収できる方法は思いつく限りだとshellのaliadを指定する方法です。

要は単純で、alias名は何でもいいんだけど例えば下記のようなものを各々のdotfilesに追記する。

alias npm-run='npm run --silent $*'

これでnpm scriptsを実行したいときはnpm-run lintとかで実行する。

ログが欲しい時やCIではnpm run lintを使う。

解決策番外編:pipeで無理やり成功させる

これは全然解決策じゃなくて、絶対にやめたほうがよいのであえて書いた。

何かというと、npmのissueやstackoverflowを眺めてると「npm scriptに|| true足せばいいよ」ってのがあって

npm scriptの実行結果を無理やりtrueに持っていけばエラーじゃないからログも出ないぜという話。

{
  "scripts" [
    "lint": "eslint src/ || true"
  ]
}

ただ、確かにログは吐かなくなるんだけどnpm scriptの実行が失敗してもコマンドがこけないので

CI等で実行してる場合は例えばeslintが失敗してもそれを補足できなくなる。

絶対にやめような^^

結論

現状、「標準出力にいらんエラーを吐かせず、npm-debug.logはちゃんと残してくれるnpm scriptの書き方」はない。

ので以下の2策に逃げるしか無い気がする。

  • npm script実行時は-sオプションをつける
  • .npmrcloglevel=silentを指定する

ただし、前者は複数人開発だと周知が面倒だし後者はいざというときにログが無くて死ぬ可能性がある。

npmのissueにもこの話題はあがっていて、dev環境用のnpm scriptsを用意しようとかいろいろ提案されているみたい。

run-scripts are too noisy while used in development · Issue #8821 · npm/npm · GitHub

個人的にはnpm慣れてるので、個人ではshellのaliasで逃げつつチームの時はこのブログ記事ぶん投げようかなという感じ。

追記

匿名の方より以下のコメントをいただきました。

f:id:sota1235:20170127155929p:plain

これであれば「標準出力にいらんエラーを吐かせず、npm-debug.logはちゃんと残してくれるnpm scriptの書き方」を実現できそうです。

具体的にはこんな感じのshellを書いて使う。

#!/bin/zsh

###
# npm run with slim
###
DOTFILES_NPM_ERROR_LOG="$HOME/.dotfiles/dist/npm_error.log"

function npmrun() {
  npm run $1 2>$DOTFILES_NPM_ERROR_LOG

  if [ ! $? -eq 0 ]; then
    echo "npm error log recorded at $DOTFILES_NPM_ERROR_LOG"
    return 1
  fi
}

もしかしたらもっと良い書き方があるかもだが、これでひとまず解決した。最高!

YAP(achimon)C::Asia Hachioji 2016に登壇しました

YAPCに登壇したぞ!

30分のトークとLTでそれぞれ登壇しました。

10分以上の発表は実は初めてでした。

年初のポエムで立てた対外発表の第一歩として応募して採択されたので頑張った。

sota1235.hatenablog.com

DIコンテナの話

DIの発表はよく見るけどDIコンテナの話はそんなに見ない気がしたので発表した。

YAPCのPはPHPのPだと聞いていたのでPHPのDIコンテナに絞って発表した。

speakerdeck.com

ESLintの話

LTではJavaScriptの話した。

30分トークで死ぬほど緊張した後だったのでどうにでもなれ精神で頭おかしい発表してしまった。

かわいいESLint実装誰か頼む|ω・`)

speakerdeck.com

反省会

  • 発表テーマがちょっとわかりづらいだけどに聴衆が少なかった。(そのおかげであまり緊張しなくて済んだのだけど)
  • 接続確認が甘くてスライドの下が見切れてた
  • 日本語が怪しい
  • LTそこそこ笑い取れて安心した

とはいえ、聴いてくれてた方はみなリアクションをくれたしご質問ももらったので最初にしてはまずまずかなと思ってる。

残念ながら2日目は参加できないのですが、参加される方は楽しんでください!!

今後

次はPHPカンファレンスに登壇したい!頑張ります。