여러 개의 개별 빌드가 단일 애플리케이션을 형성해야 합니다. 이러한 개별 빌드는 컨테이너처럼 작동하며, 빌드 간에 서로 코드를 노출하고 소비하여 단일 통합 애플리케이션을 생성할 수 있습니다.
이것은 종종 Micro-Frontends라고 알려져 있지만, 그것에만 국한되지는 않습니다.
로컬 모듈과 원격 모듈을 구별합니다. 로컬 모듈은 현재 빌드의 일부인 일반 모듈입니다. 원격 모듈은 현재 빌드의 일부가 아니며 원격 컨테이너에서 런타임에 로드되는 모듈입니다.
원격 모듈을 로드 하는 것은 비동기 작업으로 간주됩니다. 원격 모듈을 사용할 때 이러한 비동기 작업은 원격 모듈과 엔트리 포인트 사이에 있는 다음 청크 로드 작업에 배치됩니다. 청크 로드 작업 없이는 원격 모듈을 사용할 수 없습니다.
청크 로드 작업은 일반적으로 import()
를 호출하는 것이지만, require.ensure
또는 require([...])
같은 이전 구조도 지원됩니다.
특정 모듈에 대한 비동기 접근을 노출하는 컨테이너 엔트리를 통해 컨테이너가 생성됩니다. 노출된 접근은 두 단계로 구분됩니다.
1단계는 청크 로드 중에 수행됩니다. 2단계는 다른 로컬 및 원격 모듈과 인터리브된 모듈 평가 중에 수행됩니다. 이렇게 하면, 모듈을 로컬에서 원격으로 또는 그 반대로 변환해도 평가 순서가 영향을 받지 않습니다.
컨테이너를 중첩할 수 있습니다. 컨테이너는 다른 컨테이너의 모듈을 사용할 수 있습니다. 컨테이너 간의 순환 의존성도 가능합니다.
각각의 빌드는 컨테이너의 역할을 하며 다른 빌드를 컨테이너로 소비하기도 합니다. 이렇게 하면 각각의 빌드가 해당 컨테이너에서 로드 하여 다른 노출된 모듈에 접근할 수 있습니다.
공유된 모듈은 오버라이딩 가능하고 중첩된 컨테이너에 오버라이드로 제공됩니다. 일반적으로 각 빌드에서 동일한 모듈(예, 동일한 라이브러리)을 가리킵니다.
packageName
옵션을 사용하면 requiredVersion
을 찾도록 패키지 이름을 설정할 수 있습니다. 기본적으로 모듈 요청에 자동으로 추론되며, 자동 추론을 비활성화해야 하는 경우 requiredVersion
을 false
로 설정합니다.
이 플러그인은 지정된 노출 모듈로 추가 컨테이너 엔트리를 생성합니다.
이 플러그인은 컨테이너에 대한 특정 참조를 외부로 추가하고 컨테이너에서 원격 모듈을 가져올 수 있도록 합니다. 또한 컨테이너의 override
API를 호출하여 오버라이드를 제공합니다. 로컬 오버라이드 (빌드도 컨테이너인 경우 __webpack_override__
또는 override
API을 통해) 그리고 지정된 오버라이드가 모든 참조된 컨테이너에게 제공됩니다.
ModuleFederationPlugin
은 ContainerPlugin
과 ContainerReferencePlugin
을 결합합니다.
config.context
에서 확인할 수 있습니다.requiredVersion
을 사용하지 않습니다.requiredVersion
을 추출합니다./
가 있는 모듈 요청은 이 접두사의 모든 모듈 요청과 일치합니다.단일 페이지 애플리케이션의 각 페이지는 별도의 빌드에서 컨테이너 빌드로부터 노출됩니다. 또한 애플리케이션 쉘은 모든 페이지를 원격 모듈로 참조하는 별도의 빌드입니다. 이렇게 하면 각 페이지를 별도로 배포할 수 있습니다. 경로가 업데이트되거나 새 경로가 추가되면 애플리케이션 쉘이 배포됩니다. 애플리케이션 쉘은 페이지 빌드에서 중복을 방지하기 위해 일반적으로 사용되는 라이브러리를 공유 모듈로 정의합니다.
많은 애플리케이션은 각 컴포넌트가 노출된 컨테이너로 빌드 할 수 있는 공통 컴포넌트 라이브러리를 공유합니다. 각 애플리케이션은 컴포넌트 라이브러리 컨테이너의 컴포넌트를 사용합니다. 모든 애플리케이션을 다시 배포할 필요 없이 컴포넌트 라이브러리의 변경을 별도로 배포할 수 있습니다. 애플리케이션은 컴포넌트 라이브러리의 최신 버전을 자동으로 사용합니다.
컨테이너 인터페이스는 get
및 init
메소드를 지원합니다.
init
은 하나의 인수(공유 범위 객체)로 호출되는 async
호환 메소드입니다. 이 객체는 원격 컨테이너에서 공유 범위로 사용되며 호스트에서 제공된 모듈로 채워집니다.
원격 컨테이너를 런타임에 동적으로 호스트 컨테이너에 연결하는데 사용할 수 있습니다.
init.js
(async () => {
// 공유 범위를 초기화 합니다. 이 빌드와 모든 원격에서 제공된 모듈로 채웁니다
await __webpack_init_sharing__('default');
const container = window.someContainer; // 또는 다른 곳에서 컨테이너를 얻으십시오
// 컨테이너 초기화, 공유 모듈 제공 가능합니다
await container.init(__webpack_share_scopes__.default);
const module = await container.get('./module');
})();
컨테이너는 공유 모듈을 제공하려고 시도하지만, 공유 모듈이 이미 사용된 경우, 경고 및 제공된 공유 모듈이 무시됩니다. 컨테이너는 여전히 대체 수단으로 사용할 수 있습니다.
이렇게 하면 다른 버전의 공유 모듈을 제공하는 A/B 테스트를 동적으로 로드 할 수 있습니다.
예:
init.js
function loadComponent(scope, module) {
return async () => {
// 공유 범위를 초기화 합니다. 이 빌드와 모든 원격에서 제공된 모듈로 채웁니다
await __webpack_init_sharing__('default');
const container = window[scope]; // 또는 다른 곳에서 컨테이너를 얻으십시오
// 컨테이너 초기화, 공유 모듈 제공 가능합니다
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}
loadComponent('abtests', 'test123');
일반적으로, 원격은 다음 예시와 같은 URL을 사용하여 설정합니다.
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
}),
],
};
하지만 원격에 promise를 전달할 수도 있고, 이는 런타임에 해석됩니다. 위에서 설명한 get/init
인터페이스에 맞는 모듈로 이 promise를 해석해야 합니다. 예를 들어 사용할 통합 모듈의 버전을 전달하려는 경우 쿼리 매개 변수를 통해 다음과 같은 작업을 수행할 수 있습니다.
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: `promise new Promise(resolve => {
const urlParams = new URLSearchParams(window.location.search)
const version = urlParams.get('app1VersionParam')
// 이 부분은 통합 모듈 호스팅 및 버전의 관리 계획에 따라 달라집니다.
const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js'
const script = document.createElement('script')
script.src = remoteUrlWithVersion
script.onload = () => {
// 주입된 스크립트가 로드되어 window에서 사용할 수 있습니다.
// 이제 Promise를 해석할 수 있습니다.
const proxy = {
get: (request) => window.app1.get(request),
init: (...arg) => {
try {
return window.app1.init(...arg)
} catch(e) {
console.log('remote container already initialized')
}
}
}
resolve(proxy)
}
// 이 스크립트를 버전이 지정된 remoteEntry.js로 src 세트와 함께 주입합니다.
document.head.appendChild(script);
})
`,
},
// ...
}),
],
};
이 API를 사용할 때는 get/init API가 포함된 개체를 확인해야 합니다.
호스트가 원격 모듈의 메소드를 노출하여 런타임 원격 모듈의 publicPath를 설정하도록 허용할 수 있습니다.
이 접근 방식은 호스트 도메인의 하위 경로에 독립적으로 배포된 하위 응용 프로그램을 마운트 할 때 특히 유용합니다.
시나리오입니다.
https://my-host.com/app/*
에 호스팅 된 호스트 앱과 https://foo-app.com
에 호스팅 된 하위 앱이 있습니다.
하위 앱도 호스트에 마운트되므로, https://foo-app.com
은 https://my-host.com/app/foo-app
와 https://my-host.com/app/foo-app/*
를 통해 접근할 수 있습니다. 요청은 프록시를 통해 https://foo-app.com/*
로 리다이렉션 됩니다.
예제입니다.
webpack.config.js (remote)
module.exports = {
entry: {
remote: './public-path',
},
plugins: [
new ModuleFederationPlugin({
name: 'remote', // 이 이름은 엔트리 이름과 일치해야 합니다.
exposes: ['./public-path'],
// ...
}),
],
};
public-path.js (remote)
export function set(value) {
__webpack_public_path__ = value;
}
src/index.js (host)
const publicPath = await import('remote/public-path');
publicPath.set('/your-public-path');
//bootstrap app e.g. import('./bootstrap.js')
document.currentScript.src
의 스크립트 태그에서 publicPath를 유추하고 런타임에 __webpack_public_path__
모듈 변수로 설정할 수 있습니다.
예제입니다.
webpack.config.js (remote)
module.exports = {
entry: {
remote: './setup-public-path',
},
plugins: [
new ModuleFederationPlugin({
name: 'remote', // 이 이름은 엔트리 이름과 일치해야 합니다.
// ...
}),
],
};
setup-public-path.js (remote)
// 자체 로직으로 publicPath를 파생하고 __webpack_public_path__ API로 설정하세요.
__webpack_public_path__ = document.currentScript.src + '/../';
Uncaught Error: Shared module is not available for eager consumption
애플리케이션은 전 방향 호스트로 작동하는 애플리케이션을 실행하고 있습니다. 선택할 수 있는 옵션이 있습니다.
모듈을 비동기 청크에 넣지 않고 동기식으로 제공하는 Module Federation 고급 API 내에서 의존성을 eager로 설정할 수 있습니다. 이를 통해 초기 청크에서 이러한 공유 모듈을 사용할 수 있습니다. 그러나 제공된 모든 모듈과 예비 모듈은 항상 다운로드 되므로 주의하세요. 쉘 같이 애플리케이션의 한 지점에서만 제공하는 것이 좋습니다.
비동기 경계를 사용하는 것을 추천합니다. 추가 왕복을 방지하고 일반적인 성능 향상을 위해 더 큰 청크의 초기화 코드를 분할합니다.
예를 들어, 엔트리는 다음과 같습니다.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
bootstrap.js
파일을 만들고 엔트리의 콘텐츠을 이 파일로 이동한 다음, 부트스트랩을 엔트리로 가져오겠습니다.
index.js
+ import('./bootstrap');
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.render(<App />, document.getElementById('root'));
bootstrap.js
+ import React from 'react';
+ import ReactDOM from 'react-dom';
+ import App from './App';
+ ReactDOM.render(<App />, document.getElementById('root'));
이 메소드는 동작하지만 제약사항이나 단점이 있을 수 있습니다.
ModuleFederationPlugin
을 통해 의존성을 eager: true
로 설정합니다.
webpack.config.js
// ...
new ModuleFederationPlugin({
shared: {
...deps,
react: {
eager: true,
},
},
});
Uncaught Error: Module "./Button" does not exist in container.
"./Button"
은 표시되지 않지만, 오류 메세지는 유사하게 표시됩니다. 이 문제는 일반적으로 webpack beta.16에서 webpack beta.17로 업그레이드 하는 경우에 나타납니다.
ModuleFederationPlugin 내에서 다음 위치에서 exposes를 변경합니다.
new ModuleFederationPlugin({
exposes: {
- 'Button': './src/Button'
+ './Button':'./src/Button'
}
});
Uncaught TypeError: fn is not a function
원격 컨테이너가 누락되었을 가능성이 있으므로 추가되었는지 확인하세요. 사용하려는 원격 컨테이너가 로드되었지만 이 오류가 계속 나타나면 호스트 컨테이너의 원격 컨테이너 파일도 HTML에 추가하세요.
서로 다른 원격에서 여러 모듈을 로드하는 경우 원격 빌드에 대해 output.uniqueName
을 설정하여 여러 webpack 런타임 간의 충돌을 방지하는 것이 좋습니다.