かもメモ

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

Webpack 4 で Cannot assign to read only property 'exports' of object エラー

こんにちは、Webpackでのbundle化初心者です。
今までgulpで分けたJSファイルをconcatして一つにまとめていました。gulpのconcatだとファイル名を01-みたいなprefixをつけて結合順をコントロールしてました。webpackだとその辺りも解決してくれるっぽいので良いですね。(バンドルするための処理が乗るので最終的なコード量は多くなると思いますが

今回自分で分割したファイルを読み込ませていてwebpackでは問題なくbundleされたファイルが生成されたけど、ブラウザで確認すると Cannot assign to read only property 'exports' of objectというエラーが発生したのでメモ。

importmodule.exports が混在してるとよろしくないっぽい

モジュールの化はES6のimport / export の組み合わせと、CommonJSのmodule.exports / require があり、これらが混在してしまうのがよろしくないようです。 ただ、requireimport / export と使っても問題ないようです。

e.g

import / exportmodule.exports が混在するとエラー

// config.js
module.exports = {name: '佐倉 千代'};

// name.js
import config from './config';
module.exports = '野崎 梅太郎';

// app.js
import name from './name';
console.log(name);


webpackでbundle.jsは生成されるけどブラウザで確認すると
Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'

name.jsimportmodule.exports が混在しているのでNG

import / export の組み合わせにすればOK

// config.js
export default {name: '佐倉 千代'};

// name.js
import config from './config';
export default config.name;

// app.js
import name from './name';
console.log(name);

佐倉 千代 問題なく実行される

module.exportsされているものをimport / requireして export するのはエラーにならないっぽい

Cannot assign to read only property 'exports' of object '#<Object>' (mix require and export) · Issue #4039 · webpack/webpack · GitHub こちらには

// 'a.js'
module.exports = { a: 'b' };
// b.js
const a = require('./a.js').a;
export default {
   aa: a
}
Gives error:
Cannot assign to read only property 'exports' of object '#<Object>'
Appeared after upgrade webpack 2.2.0.rc.6 -> 2.2.0.

とあったのですが、
試していたWebpack v4 の環境ではmodule.exportsされたものを読み込んでexportしてもエラーにはならなかったので仕様が変わったのかもしれません。

// config.js
module.exports = {name: '佐倉 千代'};

// name.js
import config from './config';
export default config.name;

// app.js
import name from './name';
console.log(name);

佐倉 千代

require / module.exports の組み合わせにしてもエラーになる場合がある

require / module.exports の組み合わせにすればOKかと思ったのですが、
requireしたオブジェクトをそのままmodule.exportsしているとエラーになるようです。

require / module.exports で統一されてるけどエラーになるパターン

// config.js
module.exports = {name: '野崎 梅太郎'};

// name.js
const config = require('./config');
module.exports = config.name;

// app.js
const name = require('./name');
console.log(name);

Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'

最終的な部分がimport文でもエラー

// config.js
module.exports = {name: '野崎 梅太郎'};

// name.js
const config = require('./config');
module.exports = config.name;

// app.js
import name from './name';
console.log(name);

Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'

経由しているファイル(name.js)で読み込んでいるモジュールがexportだろうと、読み込んだ内容をmodule.exportsしていればエラー

// config.js
export {name: '野崎 梅太郎'};

// name.js
const config = require('./config');
module.exports = config.name;

// app.js
const name = require('./name');
console.log(name);

Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'

// config.js
export {name: '野崎 梅太郎'};

// name.js
const config = require('./config');
module.exports = config.name;

// app.js
import name from './name';
console.log(name);

Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'

module.exports が読み込んだ内容でなければOK

// config.js
module.exports = {name: '野崎 梅太郎'};

// name.js
const config = require('./config');
module.exports = '御子柴 実琴';

// app.js
import name from './name';
console.log(name);

御子柴 実琴 問題なく実行される
config.jsexportでも、app.jsrequireでもエラーにはならない

requireimport / exportと混在しても常にOK

// config.js
module.exports = {name: '堀 政行'};

// name.js
const config = require('./config');
export default config.name;

// app.js
import name from './name';
console.log(name);

堀 政行 問題なく実行される

requireimportが同じファイル内で混在していても問題ないので次のような感じでも問題なくbundleされて実行もできる

// config.js
module.exports = {name: '堀 政行'};

// name.js
cost $ = require('jquery');
import config from './config';
config.name = '鹿島 遊';
export default config.name;

// app.js
const config = require('./config');
import name from './name';
console.log(name);

鹿島 遊
問題なく実行されるけど、importrequireが同じファイルで混在してると見通しよくないので良くないって感想。

まとめ

  • ファイル内がエラーにならない組み合わせであればOK
  • import / exportimport / require / export の組み合わせで統一する
  • require / module.exports での組み合わせで統一する
    • 但しrequireで読み込んだオブジェクトをmodule.exportsするとエラー
  • importmodule.exportsが同じファイルにあるとエラー

module.exportsはトラップが多いので使わないほうが良さそう。という感想です。

と、THE Webpack 初心者って感じのエラーでした。
EMS (ES module)の世界と CJS (CommonJS)の世界があって混在してるって訳ですね。
JavaScriptもブラウザのECMAScriptとnode.jsの世界があって仕様がそれぞれ違うってのと近い感じでしょうか…
昨今のJS界隈の流れとか歴史とか含めた仕様の違い理解してないとハマりポイントっぽいって印象です。 (IEJScriptに悩まされる事は減ったけど
絶対王者jQueryが倒れ、世は将にJSフロントエンド戦国時代!
(ここ数年は勢力図が落ち着いてる感じっぽいですが、その感離れていたのでキャッチアップが大変です


[参考]

import / exportsmodule.exports / require について

違いを理解していないので、いずれ...

 

月刊少女野崎くん ネタラバ 法令遵守 野崎梅太郎

月刊少女野崎くん ネタラバ 法令遵守 野崎梅太郎

  • 発売日: 2015/01/18
  • メディア: おもちゃ&ホビー
最近作業中 月刊少女野崎くん 見てました。面白いよね

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 の前につけるキーワード。


[参考]