かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Webpack 4 production モードで console.log を削除したい

gulpでは本番環境用にconsole.logを削除するのにgulp-strip-debugした後にgulp-uglifyでminify化すれば削除できていました。

webpackで、productionモードの時だけconsole.logを削除する方法のメモ

環境

  • node v10.15.0
  • webpack v4.29.0

1. UglifyJsPlugin uglifyjs-webpack-plugin を使うパターン

プラグインのインストール

$ yarn add -D uglifyjs-webpack-plugin

webpack.config.js

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        uglifyOptions: {
          compress: {drop_console: true}
        }
      })
    ],
  },
}

webpack 4 入門 - Qiita こちらの記事によると、uglifyjs-webpack-pluginは廃止になったとありました。
自分の作ったwebpack 4.29.0の環境だとuglifyjs-webpack-pluginを使ってconsole.logの削除を行うことが出来たのでメモとして残しました。
もしかするとmodeで自動的にuglifyjsするので、その設定の上書きとかしてしまったりであまり良くないのかもしれません。(廃止になったというテキストをググっても上手く発見することが出来ず… 時間を書けて調べてないので、推察ですが...)

2. TerserPlugin terser-webpack-plugin を使うパターン

プラグインのインストール

$ yarn add -D terser-webpack-plugin

webpack.config.js

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {drop_console: true}
        }
      })
    ],
  },
}

設定はuglifyjs-webpack-pluginterser-webpack-pluginも同じ感じです。
webpackコマンドを実行すると、console.logが削除されるようになっていればOK

production mode の時だけconsole.logを削除する

developmentモードのときもconsole.logが削除されると開発しづらいので、productionモードの時だけconsole.logを削除するようにします。

モード別に webpack の 設定ファイルを分ける

webpackで読み込むconfigファイルをモード別にしている場合はproductionモードのときに使うconfigファイルにだけ設定を書けばOKです。
設定ファイルは--config <config file path>オプションで指定することが出来ます。
例えばproductionモード用の設定ファイルをwebpack.production.config.jsにした場合は次のようなコマンドになります

$ webpack --mode production --config webpack.production.config.js

長いのでpackage.jsonscriptsに設定してしまうのが良さそうです。 package.json

{
  "scripts": {
    "build": "./node_modules/.bin/webpack --mode production --config webpack.production.config.js"
  }
}

実行

$ npm run build

1つのwebpack 設定ファイル(webpack.config.js)の内部で分岐させる

あまり巨大でない設定ならファイル内で設定の分岐を作成してしまうことも出来ます。
コマンドライン--modeオプションはargvで取得することが出来るようです。

webpack.config.js ( TerserPlugin 版 )

const TerserPlugin = require('terser-webpack-plugin');
const webpackConfig = {
  mode: "development",
  // 略
  optimization: {},
};

module.exports = (env, argv) => {
  const is_production = argv.mode === 'production';
  if( is_production ) {
    webpackConfig.mode = 'production'; // もしかしたら不要かも
    webpackConfig.optimization.minimizer = {
      new TerserPlugin({
        terserOptions: {
          compress: {drop_console: true}
        },
      }),
    }
  }
  return webpackConfig;
}

module.exportsで実行する関数内だと渡される引数argvオブジェクトからargv.modeで取得することが出来ますが、そこ以外だとprocess.argvが配列なのでオプションの渡し方によってはモードの判別が大変そうです。

webpack --mode production コマンドを実行するとconsole.logが削除され、developmentモードのwebpackコマンドだとconsole.logが残ったままにできるようになりました\\ ٩( ᐛ )و//

gulpに慣れ親しんでいたから、webpackの場合分けの設定作るの大変でした。
何より検索すると色々出てきすぎて、プラグインは公式サイトでも検索しづらく、どれが最新な情報なのか判別するのがとても難しかったです…
( CSSコンパイルまでwebpackでやるとwebpackの設定ファイルが秘伝のタレになりませんかねコレ...???


[参考]

gulp-strip-debug は console.logvoid 0に置き換える。uglifyするとvoid 0は削除される

I did previously remove it, but JS has so many edge-cases that it's safer to replace it with a void statement. See sindresorhus/strip-debug#1. Void is a noop anyways, so doesn't have any negative effect to leave it in.

Uglify is able to remove it since knows about all edge cases and how to handle them.
void 0; · Issue #2 · sindresorhus/gulp-strip-debug · GitHub

JavaScript async / await で並行処理

前回のあらすじ

直列処理

async / await 、非同期処理を順番に実行(直列処理)を簡単に書くことができます。

async function sleepSquareFunc(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve( x * x );
    }, 1000); 
  });
}

async function sumSerialFunc() {
  const a = await sleepSquareFunc(2);
  const b = await sleepSquareFunc(4);
  const c = await sleepSquareFunc(6);
  return (a + b + c) * 2;
}
const sum = serialFunc();
// => sum: 112

上記の例ではa, b, cを順番に求めますが、awaitは処理を止めて結果が返されるのを待つので最終的な結果を返すまでに3秒は待つ必要があります。
awaitさせる関数に関連性があって順番に処理を行う必要があるのであれば問題ないのですが、上記のような関連性が無い非同期処理なら逐一awaitで待たせて処理をする必然性がありません。

並行処理

Promise.allを使う
async functionはPromiseを返す関数なので、並行処理したいものはPromise.allで取得してしまえばOK。

async function sleepSquareFunc(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve( x * x );
    }, 1000); 
  });
}

async function sumParallelFunc() {
  const a = sleepSquareFunc(2);
  const b = sleepSquareFunc(4);
  const c = sleepSquareFunc(6);
  const [aa, bb, cc] = await Promise.all([a, b, c]);
  return (aa + bb + cc) * 2;
}
const sum = serialFunc();
// => sum: 112

非同期処理をループで扱う

ループの際にawaitが処理を止めることを理解しておかないと、意図せず上記の直列処理にしてしまいパフォーマンスの悪いコードになってしまう可能性があります。

ループ内でawaitを使用して直列処理になってしまうパターン

async function calculationByLoop(arg) {
  let sum = 0
  for(let v of arg) {
    let squareV = await sleepSquareFunc(v);
    // 直列処理する必要はないが 1000ms 待機してしまう
    console.log(squareV);
    sum += squareV * 2;
  }
  const endAt = performance.now();
  return sum;
}
const sum = calculationByLoop([2, 4, 6]);
// => sum: 112

Array.forEach は何も返さないのでawaitに使えない

Array.prototype.forEachundefinedを返すのでasync / awaitで使うことができない

async function calculationForEach(arg) {
  let sum = 0;
  const f = await arg.forEach( async (val) => {
    const squareV = await sleepSquareFunc(val);
    sum += squareV * 2;
  });
  console.log(f); // => undefined
  return sum;
}
const sum = calculationForEach([2, 4, 6]);
// => sum: 0

forEachのループが終わるまで待機されることがないので結果を待つ前にsumが返される。

Array.reduce はreturnする値のawaitの付け方次第で並列処理のように扱われるっぽい

配列の値に手を加えて合算するような場合であれば、Array.prototype.reduceは値を返すので、内部のコールバックにasync, awaitを使えば、次のループに引数としてPromiseなオブジェクトが渡り並行処理のように処理できるようです。(試してみるまでてっきり直列処理になるものだと思っていました。)

async function calculationByReduce(arg) {
  const sum = await arg.reduce( async (x, add) => {
    const a = await sleepSquareFunc(add);
    console.log('reduce', x, a);
    // return 値にawait を付けないと、待たずに次のループに入ってしまう
    return await x + a * 2;
  }, 0);
  return sum;
};
const sum = calculationByReduce([2, 4, 6]);
// reduce 0 4
// reduce Promise { 8 } 16
// reduce Promise { 40 } 36
// => sum: 112

reduceループに渡される引数がPromiseオブジェクトになり値が戻されるまでプレースホルダーのように計算結果を待ちつつも、ループ処理そのものはawaitせずに先に進むので結果的に並行処理のようになっているという印象です。
なので、reduce内でreturnする値にどうawaitをつけるかによっては直列処理のようになったり、値が返されるまで待たないものになったりする可能性もありそうです。

Array.mapで配列を展開してPromise.allに渡す

Promiseを返す処理にArray.prototype.mapで展開してしまい、Promise.allに渡してしまえば、それぞれの非同期処理を並行処理することができます。

async function calculationParallel(arg) {
  const all = await Promise.all( arg.map( async (val) => {
    const squareV = await sleepSquareFunc(val);
    return squareV * 2;
  } ) );
  const sum = all.reduce((x, y) => x + y);
  return sum;
};
const sum = calculationParallel([2, 4, 6]);
// => sum: 112

取得される値が配列になり、そこからループで処理などが必要になるので処理的にはreduceより多くなると思いますが、 個人的にPromise.allにしたほうが見通しは良いかなという印象です。

ただしPromise.allは渡された処理の1つでもrejectになると、その場でrejectとして返されてしまうので、並行処理して取得できた値だけでゴニョるような用途の場合は呼び出す関数がrejectを返さないようにする工夫が必要になりそうです。
reject含め全ての処理が終わるまで並行処理で待つ関数が欲しい。

おまけ

アロー関数でasync functionを書く時はこんな感じ

const functionName = async () => { ... }

async はあくまで function の前につけるキーワード。


[参考]

JavaScript 今更の async / await

async function は Promose を返す

  • returnすると返される値がPromise.resolveされる
  • throwすると値や例外がPromise.rejectされる
  • 何も返さないと関数終了時にundefinedreturn(resolove)される
// return = Promise.resolve()
async function resolveFunc() {
  return 'this resolve';
}

resolveFunc()
.then((res) => console.log(res))
.catch((err)=> console.log(`ERROR ${err}`));
// => this resolve


// throw = Promise.reject()
async function rejectErrorFunc() {
  throw new Error('throw Error');
}

rejectErrorFunc()
.then((res) => console.log(res))
.catch((err)=> console.log(`ERROR ${err}`));
// => ERROR Error: throw Error

// 値をthrow しても reject 扱い
async function throwValueFunc() {
  throw 'throw Value';
}

throwValueFunc()
.then((res) => console.log(res))
.catch((err)=> console.log(`ERROR ${err}`));
// => ERROR throw Value

// return なら返す値が Error オブジェクトでも resolve
async function returnErrorFunc() {
  return new Error('retun Error');
}

returnErrorFunc()
.then((res) => console.log(`RESOLVE ${res}`))
.catch((err)=> console.log(`ERROR ${err}`));
// => RESOLVE Error: retun Error

// 何も返さない関数
// 関数終了時に undefined が resolve される
async function noReturnFunc() {
  // do't return anything
}

noReturnFunc()
.then((res) => console.log(`RESOLVE ${res}`))
.catch((err)=> console.log(`ERROR ${err}`));
// => RESOLVE undefined

await は何をしているか

  • awaitキーワードはasync function内でしか使用できない
  • awaitキーワードで呼び出した関数からPromiseが返されるまで、自身のあるasync functionの処理を止めて待機する
function waitSquareFunc(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x * x);
    }, 500);
  });
}

// awaitキーワードはasync内でしか使用できない
async function getSquareAndAdd(x, y) {
  // awaitキーワードがあると値が帰ってくるまで処理が止まる
  const res = await waitSquareFunc(x);
  // Promise.resolve
  return res + y;
}

// async function を呼び出しているので Promise が返される
getSquareAndAdd(2, 1)
  .then((res) => console.log(res))
  .catch((err) => console.log(err));
// => 5

// async / await で何も返さない場合も関数が終了時の undefined が resolve される
async function awaitNoReturnFunc() {
  // ここでPromiseが返されるまで待機して
  const res = await waitSquareFunc(2);
  // return undefined される
}

awaitNoReturnFunc()
.then((res) => console.log(`RESOLVE ${res}`))
.catch((err)=> console.log(`ERROR ${err}`));
// => RESOLVE undefined

Promiseで書くとこんな感じ

function waitSquareFunc(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x * x);
    }, 500);
  });
}

function getSquareAndAddByPromise(x, y) {
  return waitSquareFunc(x)
    .then((res) => {return res + y})
    .catch((err) => {throw err});
}

getSquareAndAddByPromise(2, 1)
  .then((res) => console.log(res))
  .catch((err) => console.log(err));
// => 5

エラーハンドリング

try-catchでエラーハンドリング

// isError ... true -> Promise.reject / false -> throw new Error
function throwErrorFunc(isError) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        if( isError ) {
          reject('Reject ERROR');
        } else {
          // throw new Error されると catch 節に処理が流れる
          throw new Error('Throw new ERROR');
          console.log('ココは実行されない');
          resolve('NO ERROR');
        }
      } catch(e) {
        console.log('> ERROR CATCH');
        reject(e.message);
      }
    }, 500);
  });
}

async function errorHandlingTry_catch(isError) {
  try {
    const res = await throwErrorFunc(isError);
    console.log('>> NOT IN CATCH');
    return res;
  } catch(err) {
    console.log('>> TRY-CATCH ERROR');
    throw err;
  }
}

// throwErrorFunc で reject error
errorHandlingTry_catch(true)
  .then((res) => console.log(res, 'then'))
  .catch((err) => console.log(err, 'catch'));
// => >> TRY-CATCH ERROR
// => Reject ERROR catch

// throwErrorFunc で throw new Error
errorHandlingTry_catch(false)
  .then((res) => console.log(res, 'then'))
  .catch((err) => console.log(err, 'catch'));
// => > ERROR CATCH
// => >> TRY-CATCH ERROR
// => Throw new ERROR catch

async function 内ではtry-catchしなくてもawaitで呼び出されている関数でPromise.rejectされると、そのままasync functionの呼び出し元にthrowされる

async function errorHandling(isError) {
  const res = await throwErrorFunc(isError);
  console.log('NOT IN CATCH');
  return res;
}

// throwErrorFunc で reject error
errorHandling(true)
  .then((res) => console.log(res, 'then'))
  .catch((err) => console.log(err, 'catch'));
// => Reject ERROR catch

// throwErrorFunc で throw new Error
errorHandling(false)
  .then((res) => console.log(res, 'then'))
  .catch((err) => console.log(err, 'catch'));
// => > ERROR CATCH
// => Throw new ERROR catch

async / await で Fetch してみる

// fetch でエラーになるとPromise.rejectになるのでtry-catchは省略可
const getDataByFetchAPI = async () => {
  const res = await fetch(api);
  // fetchAPIは res.json() で Promiseを返すのでawaitを2重にする必要がある
  const data = await res.json();
  return data;
};

getDataByFetchAPI()
  .then((res) => console.log(res))
  .catch((err) => console.log(err.message));

まとめ

遥か昔のCallback hellなExpressプロジェクトを見ていたので、JavaScriptの非同期処理すごく見やすく書けるようになったんだね。ほえーって感じです。(Promiseでも十分見やすいですよ...

asyncは必ずしもawaitを使う必要はなく、Promiseを返す関数。
awaitを使えばコールバックなネストになった文でなく非同期処理の待機を書けるから完結でとても便利だけど、処理を止めるという意識がないと不用意にループの中で使って処理時間が長くなってしまうとかありそうなので、awaitが何をしているのか理解して並行処理したほうが良いのかとか設計するのが良さそうって思いました。

Promiseの記事を書いた時に次はasync/awaitだ!って言ったままだったので、書けてよかった。 (Promise.resolve


[参考]