うならぼ

申し訳程度のアフィリエイトとか広告とか解析とかは/aboutを参照

Mastodonで見つけた循環import

Mastodonのフロント側で、あるモジュールの関数において、そのモジュールの依存モジュールが一部読み込まれていないらしい、という問題がありました。こんなエラー。

Uncaught TypeError: Cannot read property 'a' of undefined
    at Object.configureStore [as a] (98:31)
    at Object.eval (94:59)
    at eval (94:199)
    at Object.<anonymous> (common.js:1197)
    at __webpack_require__ (common.js:55)
    at Object.eval (98:8)
    at eval (98:55)
    at Object.<anonymous> (common.js:1244)
    at __webpack_require__ (common.js:55)
    at Object.eval (589:7)

下のコードで configureStore() を呼び出そうとしていたのですが、何故か loadingBarMiddleware が読み込まれない。調べていった結果、そもそも以下のimport群のうち hydrateAction の行までしか実行されていないことがわかりました。

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import appReducer, { createReducer } from '../reducers';
import { hydrateStoreLazy } from '../actions/store';
import { hydrateAction } from '../containers/mastodon';
import loadingBarMiddleware from '../middleware/loading_bar';
import errorsMiddleware from '../middleware/errors';
import soundsMiddleware from '../middleware/sounds';

export default function configureStore() {
  ...
}

上記のコードはWebpackによって次のようなコードに変換されています。

/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (immutable) */ __webpack_exports__["a"] = configureStore;
/* harmony export (immutable) */ __webpack_exports__["b"] = injectAsyncReducer;
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_redux__ = __webpack_require__(/*! redux */ 169);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_redux_thunk__ = __webpack_require__(/*! redux-thunk */ 370);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_redux_thunk___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_redux_thunk__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__reducers__ = __webpack_require__(/*! ../reducers */ 371);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__actions_store__ = __webpack_require__(/*! ../actions/store */ 35);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__middleware_loading_bar__ = __webpack_require__(/*! ../middleware/loading_bar */ 430);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__middleware_errors__ = __webpack_require__(/*! ../middleware/errors */ 431);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__middleware_sounds__ = __webpack_require__(/*! ../middleware/sounds */ 432);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_7__containers_mastodon__ = __webpack_require__(/*! ../containers/mastodon */ 94);
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

function configureStore() {
  ...
}

全体を包む無名関数の中で、import文はそれぞれ __webpack_require__() の呼び出しで実現されていることがわかります。JavaScriptにおいて関数宣言は定義ごと巻き上げられるので、もしimportの最中に configureStore() が呼ばれてしまったら、同じような結果を得ることができそうです。

ん?/containers/mastodon だって?

import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
...
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
const { localeData, messages } = getLocale();
addLocaleData(localeData);

export const store = configureStore();
const initialState = JSON.parse(document.getElementById('initial-state').textContent);

…ありますね、configureStore()。変数の初期化に使われているので、モジュールを読み込んだだけで実行されます。結果として、configureStoreモジュールのimportをすべて終える前に、間接的に configureStore() が呼ばれてしまう、ということになります。

逆に mastodon モジュールから読み込んだ場合も循環してしまいますが、configureStore モジュールを読み込んだだけで mastodon モジュールの関数が呼び出されることはないので、この場合は問題ありません。

__webpack_require__() は各モジュールを実行する前に、先にキャッシュに登録を行います。一方先に載せたモジュールのコードを見ると、importより先にexportした関数を登録していることがわかります。ですから configureStore モジュールの読み込みが完了する前に mastodon モジュールから configureStore() 関数が呼び出せた、というわけです。