CRA + Module Federation 試す
Module Federation を CRA(create react app) で作成した react app と共に使用することが可能なのか試していく。
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
動作確認
host app 起動
$ cd host-app
$ yarn start
remote app 起動
$ cd remote-app
$ yarn start
Remote app から component を export
RemoteContent
component を作成
remote app から export して、host app に組み込む component を作る
import React from 'react';
export const RemoteContent = () => {
return (
<div>
<h3>Hi! I'm from Remote App!</h3>
</div>
);
};
export default RemoteContent;
remote app 側でdousakakuninn
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'));
RemoteContent
component を export
webpack.config.js を修正し、RemoteContent
を expose する
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'],
},
},
}),
],
};
CRA の webpack について
webpack の version
- ModuleFederation は Webpack5 の機能なので CRA も webpack5 を利用している必要がある
- CRA は v5 から webpack5 に対応している (https://github.com/facebook/create-react-app/releases/tag/v5.0.0)
CRA で webpack を編集
選択肢を調べる
-
eject する (https://create-react-app.dev/docs/available-scripts)
-
react-scripts を fork (https://create-react-app.dev/docs/alternatives-to-ejecting/)
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.
- CRACO を使う(https://github.com/gsoft-inc/craco)
Create React App Configuration Override is an easy and comprehensible configuration layer for create-react-app.
- 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 ではないのが気がかり...
CRACO を使って webpack config を編集してみる
注意
Support
- Create React App (CRA) 4.*
CRA v5 はまだ support されてないがやってみる
Install
$ yarn add @craco/craco
動作確認
pacage.json の react-scripts を craco に置き換える
"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
を空で作る
module.exports = {}
もう一度試す
$ yarn start
とりあえず動いた
craco を利用した wepback config の編集
CRA の built-in の webpack confiig を craco で再現したもの
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: {}
}
]
};
CRA を Module Federation 対応
craco を使って webpack config を編集できるようになったので、home app を Module Federation 対応していく。
ModuleFederation plugin setup
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 しているが、これが原因っぽい
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
に移動させて、
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.js
を index.js
から import
import('./bootstrap')
もう一度 dev server 起動
$ yarn start
RemoteContent
組み込み
App
component に remote app の Remote
component を組み込む
+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 が読み込まれていることを確認。
完了!
まとめ
CRA に Module Federation 対応する手順
- craco install
- index.js の内容を別のファイルに移し、そのファイルを import
-
carco.config.js
で ModuleFederationPlugin の設定
craco-module-federation が Production Ready になったら検討したいのでチェックしていく