Closed13

CRA + Module Federation 試す

nbstshnbstsh

Module Federation を CRA(create react app) で作成した react app と共に使用することが可能なのか試していく。

nbstshnbstsh

Project Setup

こんな感じでセットアップしていく↓

  • host app (CRA)
  • remote app (React)

(今回は便宜上全て js で作っていく)

$ mkdir mf-cra-sample
$ cd mf-cra-sample 

host app を CRA で準備

$ npx create-react-app host-app

remote app を準備

$ npx create-mf-app
? Pick the name of your app: remote-app
? Project Type: Application
? Port number: 3001
? Framework: react
? Language: javascript
? CSS: CSS

Project structure

├── host-app
|   ├── README.md
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   └── src
└── remote-app
    ├── node_modules
    ├── package.json
    ├── src
    ├── webpack.config.js
    └── yarn.lock
nbstshnbstsh

動作確認

host app 起動

$ cd host-app
$ yarn start

remote app 起動

$ cd remote-app
$ yarn start

nbstshnbstsh

Remote app から component を export

RemoteContent component を作成

remote app から export して、host app に組み込む component を作る

remote-app/src/RemoteContent.jsx
import React from 'react';

export const RemoteContent = () => {
  return (
    <div>
      <h3>Hi! I'm from Remote App!</h3>
    </div>
  );
};

export default RemoteContent;

remote app 側でdousakakuninn

remote-app/src/App.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
+import RemoteContent from './RemoteContent';

const App = () => (
  <div className='container'>
    <div>Name: remote-app</div>
    <div>Framework: react</div>
    <div>Language: JavaScript</div>
    <div>CSS: Empty CSS</div>

+    <RemoteContent />
  </div>
);
ReactDOM.render(<App />, document.getElementById('app'));

nbstshnbstsh

RemoteContent component を export

webpack.config.js を修正し、RemoteContent を expose する

remote-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  // other config ...

  plugins: [
    // other config ...

    new ModuleFederationPlugin({
      name: 'remote_app',
      filename: 'remoteEntry.js',
      remotes: {},
+      exposes: {
+        './RemoteContent': './src/RemoteContent.jsx',
+      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
  ],
};
nbstshnbstsh

CRA の webpack について

webpack の version

CRA で webpack を編集

選択肢を調べる

Ejecting lets you customize anything, but from that point on you have to maintain the configuration and scripts yourself. This can be daunting if you have many similar projects. In such cases instead of ejecting we recommend to fork react-scripts and any other packages you need. This article covers how to do it in depth. You can find more discussion in this issue.

Create React App Configuration Override is an easy and comprehensible configuration layer for create-react-app.

nbstshnbstsh
  • craco-module-federation を使う

THIS IS NOT PRODUCTION READY Webpack 5 support of CRA is still in its alpha phase and there might be breaking changes in either CRA5 or this plugin in the future.

Add module-federation support to your CRA5 project without ejecting and losing update support of react-scripts

craco-module-federation という CRA に Module Federation を取り入れる、正しく今回欲していた library が存在したが、

THIS IS NOT PRODUCTION READY

Production Ready ではないのが気がかり...

https://github.com/hasanayan/craco-module-federation

nbstshnbstsh

CRACO を使って webpack config を編集してみる

https://github.com/gsoft-inc/craco

注意

Support

  • Create React App (CRA) 4.*

CRA v5 はまだ support されてないがやってみる

nbstshnbstsh

Install

$ yarn add @craco/craco

動作確認

pacage.json の react-scripts を craco に置き換える

package.json
"scripts": {
-   "start": "react-scripts start",
+   "start": "craco start",
-   "build": "react-scripts build",
+   "build": "craco build"
-   "test": "react-scripts test",
+   "test": "craco test"
}
$ yarn start

エラー出た

Error: craco: Config file not found. check if file exists at root (craco.config.ts, craco.config.js, .cracorc.js, .cracorc.json, .cracorc.yaml, .cracorc)

confi file を先に作らないといけないみたい。

host-app/craco.config.js を空で作る

host-app/craco.config.js
module.exports = {}

もう一度試す

$ yarn start

とりあえず動いた

nbstshnbstsh

craco を利用した wepback config の編集

https://github.com/gsoft-inc/craco/blob/master/packages/craco/README.md#configuration

CRA の built-in の webpack confiig を craco で再現したもの
craco.config.js
const { when, whenDev, whenProd, whenTest, ESLINT_MODES, POSTCSS_MODES } = require("@craco/craco");

module.exports = {
    reactScriptsVersion: "react-scripts" /* (default value) */,
    style: {
        modules: {
            localIdentName: ""
        },
        css: {
            loaderOptions: { /* Any css-loader configuration options: https://github.com/webpack-contrib/css-loader. */ },
            loaderOptions: (cssLoaderOptions, { env, paths }) => { return cssLoaderOptions; }
        },
        sass: {
            loaderOptions: { /* Any sass-loader configuration options: https://github.com/webpack-contrib/sass-loader. */ },
            loaderOptions: (sassLoaderOptions, { env, paths }) => { return sassLoaderOptions; }
        },
        postcss: {
            mode: "extends" /* (default value) */ || "file",
            plugins: [require('plugin-to-append')], // Additional plugins given in an array are appended to existing config.
            plugins: (plugins) => [require('plugin-to-prepend')].concat(plugins), // Or you may use the function variant.
            env: {
                autoprefixer: { /* Any autoprefixer options: https://github.com/postcss/autoprefixer#options */ },
                stage: 3, /* Any valid stages: https://cssdb.org/#staging-process. */
                features: { /* Any CSS features: https://preset-env.cssdb.org/features. */ }
            },
            loaderOptions: { /* Any postcss-loader configuration options: https://github.com/postcss/postcss-loader. */ },
            loaderOptions: (postcssLoaderOptions, { env, paths }) => { return postcssLoaderOptions; }
        }
    },
    eslint: {
        enable: true /* (default value) */,
        mode: "extends" /* (default value) */ || "file",
        configure: { /* Any eslint configuration options: https://eslint.org/docs/user-guide/configuring */ },
        configure: (eslintConfig, { env, paths }) => { return eslintConfig; },
        pluginOptions: { /* Any eslint plugin configuration options: https://github.com/webpack-contrib/eslint-webpack-plugin#options. */ },
        pluginOptions: (eslintOptions, { env, paths }) => { return eslintOptions; }
    },
    babel: {
        presets: [],
        plugins: [],
        loaderOptions: { /* Any babel-loader configuration options: https://github.com/babel/babel-loader. */ },
        loaderOptions: (babelLoaderOptions, { env, paths }) => { return babelLoaderOptions; }
    },
    typescript: {
        enableTypeChecking: true /* (default value)  */
    },
    webpack: {
        alias: {},
        plugins: {
            add: [], /* An array of plugins */
            add: [
                plugin1,
                [plugin2, "append"],
                [plugin3, "prepend"], /* Specify if plugin should be appended or prepended */
            ], /* An array of plugins */
            remove: [],  /* An array of plugin constructor's names (i.e. "StyleLintPlugin", "ESLintWebpackPlugin" ) */
        },
        configure: { /* Any webpack configuration options: https://webpack.js.org/configuration */ },
        configure: (webpackConfig, { env, paths }) => { return webpackConfig; }
    },
    jest: {
        babel: {
            addPresets: true, /* (default value) */
            addPlugins: true  /* (default value) */
        },
        configure: { /* Any Jest configuration options: https://jestjs.io/docs/en/configuration */ },
        configure: (jestConfig, { env, paths, resolve, rootDir }) => { return jestConfig; }
    },
    devServer: { /* Any devServer configuration options: https://webpack.js.org/configuration/dev-server/#devserver */ },
    devServer: (devServerConfig, { env, paths, proxy, allowedHost }) => { return devServerConfig; },
    plugins: [
        {
            plugin: {
                overrideCracoConfig: ({ cracoConfig, pluginOptions, context: { env, paths } }) => { return cracoConfig; },
                overrideWebpackConfig: ({ webpackConfig, cracoConfig, pluginOptions, context: { env, paths } }) => { return webpackConfig; },
                overrideDevServerConfig: ({ devServerConfig, cracoConfig, pluginOptions, context: { env, paths, proxy, allowedHost } }) => { return devServerConfig; },
                overrideJestConfig: ({ jestConfig, cracoConfig, pluginOptions, context: { env, paths, resolve, rootDir } }) => { return jestConfig },
            },
            options: {}
        }
    ]
};
nbstshnbstsh

CRA を Module Federation 対応

craco を使って webpack config を編集できるようになったので、home app を Module Federation 対応していく。

ModuleFederation plugin setup

host-app/craco.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;

module.exports = {
  webpack: {
    plugins: {
      add: [
        new ModuleFederationPlugin({
          name: 'host_app',
          filename: 'remoteEntry.js',
          remotes: {
            'remote-app': 'remote_app@http://localhost:3001/remoteEntry.js',
          },
          exposes: {},
          shared: {
            ...deps,
            react: {
              singleton: true,
              requiredVersion: deps.react,
            },
            'react-dom': {
              singleton: true,
              requiredVersion: deps['react-dom'],
            },
          },
        }),
      ] /* An array of plugins */,
    },
  },
};

一旦動くか確認

$ yarn start

画面真っ白...
エラーでてた...

Uncaught Error: Shared module is not available for eager consumption: webpack/sharing/consume/default/react/react

エラー対応

CRA は index.js で render しているが、これが原因っぽい

host-app/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

index.js の中身を bootstrap.js に移動させて、

host-app/src/bootstrap.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

bootstrap.jsindex.js から import

host-app/src/index.js
import('./bootstrap')

もう一度 dev server 起動

$ yarn start

RemoteContent 組み込み

App component に remote app の Remote component を組み込む

host-app/src/App.js
+import { RemoteContent } from 'remote-app/RemoteContent';
import './App.css';
import logo from './logo.svg';

function App() {
  return (
    <div className='App'>
+      <RemoteContent />

      <header className='App-header'>
        <img src={logo} className='App-logo' alt='logo' />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className='App-link'
          href='https://reactjs.org'
          target='_blank'
          rel='noopener noreferrer'
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

RemoteContent が表示されているか確認

Network タブから、ちゃんと http://localhost:3001 から component が読み込まれていることを確認。

完了!

nbstshnbstsh

まとめ

CRA に Module Federation 対応する手順

  • craco install
  • index.js の内容を別のファイルに移し、そのファイルを import
  • carco.config.js で ModuleFederationPlugin の設定
このスクラップは2022/04/21にクローズされました