@edynamix/mf-同位素
CSS隔离和Angular重叠辅助器用于微前端.
安装
bun add @edynamix/mf-isolation@latest
运行时边界
import { prepareMfBoundary } from '@edynamix/mf-isolation';
const disposeBoundary = prepareMfBoundary(element, {
scopeId: 'portal',
});
// When the microfrontend unmounts:
disposeBoundary();
构建时间 CSS 隔离
全球和供应商支助:
import { scopeRemoteCss } from '@edynamix/mf-isolation/postcss';
export default {
css: {
postcss: {
plugins: [scopeRemoteCss({ scopeId: 'portal' })],
},
},
};
主机和遥控器必须使用相同的 scopeId。两个 PostCSS 插件将普通全局和供应商选择器置于零特性下 :where([data-edyn-mf="<scope-id>"]) 边界。 原始文件的后人,如 html body#app button 或者说 html body button 保持根的重量,它们开始于: :where([data-edyn-mf="<scope-id>"]):not(<carrier>)时 :where() 无重和载体(例如: edyn-mf-root edyn-mf-root#edyn-mf-root 页:1 html body#app)具有与被替换的根相同的ID,类和类型计数,并且从未与范围元素相匹配. 因此,模块会像独立页上一样,在角材料上胜负.
模块页面有一个 <html> 和一个 <body>,所以一个需要第二个的选手 从来没有匹配: html body html body .x (一个全局部分在装入时会编译到什么) html body {}, (中文(简体) ), body .a body .b,或组合符后文档根(.x html, (中文(简体) ), .x :root) (中文(简体) ). 扫描会把这些根变成边界,使规则匹配成为主机,因此PostCSS插件都投放出这样的选择器,以及当没有选择器(0.036或更后)时的规则. 下方的运行时间反射器 留下了书面的, 里面也没有匹配.
根字体大小和雷姆
rem 总是与页面相对应 <html>甚至在阴影之根里 一个模块页面,其大小是自己的根(html, body { font-size: 12px })从这个大小绘制出每个雷姆长度;主机,相同的长度会使用主机页面的根. PostCSS 两种插件都因此记录了可解析 font-size 或者说 font 显示大小 html 或者说 :root (px,%,em,rem,绝对单位和大小关键字,与起首的16px相对) --edyn-mf-rem 在模块边界上,在设定它的规则之后 @media 条件和条件 !important 继续 大小 body (或 html body)单独放出rem,就像它在模块自己的页面上那样. 无法在构建时解决的大小( A)var(), (中文(简体) ), calc() 与 var(),取景器单元,系统字体)作为 PostCSS 警告报告;Rem 然后保留主机根大小. 从未大小的 CSS 和以前一样被排放出来 .
跑步时, provideMfOverlayRoot() 财务报告和审定财务报表 provideMfOverlayRootFromToken() 开始 followModuleRootFontSize(shadowRoot) 对于阴影根, 覆盖根坐入, 从环境初始化器( 在第一个组件完成之前) 直到注射器被摧毁。 一旦根中的样式表宣布 --edyn-mf-rem,每个雷姆长度变为 calc(<n> * var(--edyn-mf-rem, 1rem)): 在其样式表,包括组件样式中,Angular稍后添加,并内入 style 属性,包括由模板、绑定或代码后插入或更改的属性。 两者均通过CSSOM(rule.style 财务报告和审定财务报表 element.style),这样 @media, (中文(简体) ), @container 财务报告和审定财务报表 @supports 条件、字符串和 url() 保留他们的短信, !important 留下,重写是独有能力。 在单个属性测试中跳过没有未跟随回放长度(Matrial的波纹和上覆位置)的内置样式,一个其CSS从未大小其根的模块不支付任何报酬:没有启动内置观察者. 附加规则 insertRule() 至已重写的工作表,以及嵌入阴影根内的样式,不重写.
运行时主机模块样式
一个主机呼叫 adaptHostedModuleStyles(shadowRoot) 从 @edynamix/mf-isolation 在它创建的每个模块的阴影根上,以及它在卸载时返回的函数。 它适应一个模块在运行时给根带来的东西,没有模块重建:
- rem 遵循模块本身的根字体大小, 如
followModuleRootFontSize()执行以上; - 规则一个库在运行时对文档根进行注入,在阴影根内永远不匹配(Angular Matery's
html { --mat-focus-indicator-display: block; }左键盘焦点环关闭),所以它们会像PostCSS插件在构建时所做的那样移动到阴影主机上:html, (中文(简体) ),body, (中文(简体) ),:root财务报告和审定财务报表html body变成:host,以及html .x, (中文(简体) ),body > .x或者说:root .x变成:host .x财务报告和审定财务报表:host > .x具有自身条件的根(html.dark, (中文(简体) ),body#app, (中文(简体) ),:root[dir=rtl])与任何已经命名的规则一样,是单独存在的:host或者说data-edyn-mf。 。 。 .rescopeDocumentRootSelectors(selectorText)是纯选取步骤,并报告它留下了多少个合格根; - 为遗留导航的上行栏杆所写的粘贴头被写到主机内容区的上行:旧页被滚动到49px栏下,故为粘贴
top49px 或超过49px(top: 49px, (中文(简体) ),top: 50px)向上移动49px,在样式表规则和内置样式中从代码(位置-粘接指令')中调取模块集position: sticky; top: 50px),其优先级保留,每分值一次,模块写出(0.034或以后)。 较小的顶部0, (中文(简体) ),1px,用于已滚动到栏下方的页和面板,var(), (中文(简体) ),%财务报告和审定财务报表calc()值,而非粘滞规则则单独存在.
在舱外安全 followModuleRootFontSize() 在同一根上(0.0.31或更后):样式表的修改会通过CSSOM,没有变异观察者看到,每个步骤都是一能的,已经遵循的内线样式从此不再看起来像工作,因此在对方的写作上也没有边行.
来自主机模块的文件查询
通过全球文件找到自身要素的模块(document.querySelector('.week-wrapper'), (中文(简体) ), this.document.getElementById('notes-tab-3') 通过角星 DOCUMENT)在自己的页面中找到它们;主机,它们生活在它的阴影根中,文档查询从不输入,所以相同的呼叫返回 null 或者什么都没有。 一个主机呼叫 scopeDocumentQueries(shadowRoot) 从 @edynamix/mf-isolation 旁边 adaptHostedModuleStyles() 在它创建的每个模块的阴影根上,以及它在卸载时返回的函数.
当一个根被注册, querySelector, (中文(简体) ), querySelectorAll, (中文(简体) ), getElementById 财务报告和审定财务报表 getElementsByClassName 在根的文档上,首先运行为正常运行,并在发现任何结果时保持其结果. 只有空结果会回落到已注册的根上,最新的第一(在模块中切换到的模块答案之前的出出模块),第一个有匹配答案的根. 要求任何其他文件,例如iframe的或来自 createHTMLDocument(){\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}没有动静 文档原型为任意数量的根被补丁一次,与页面上本套件的其他拷贝共享,并在上个根停止时恢复;一种方法是其他人在我们之后包裹的包装,我们的包被它通过.
权衡: querySelectorAll 财务报告和审定财务报表 getElementsByClassName 静态 NodeLists,不是活集,所以一个模块持有一个并期望它更新,必须再次查询. 一个文档匹配总是获胜,所以一个请求一个类或ID的模块主页也会像以前那样使用得到主机的元素. 依赖的页面上的代码 null 对于一个选中的模块,其中的一个模块匹配,现在获得模块的元素. getElementsByTagName 不被补丁: 标签查询几乎总是符合主机本身的元素, 因此一个倒计时会很少帮助并可以将模块的 <link> 或者说 <style> 来检查页面.
有维特的角
角化应用程序必须使用角化-感知插件,因此组件 CSS 仍然与 Emiled 视图封装相容 :
import { scopeAngularRemoteCss } from '@edynamix/mf-isolation/postcss';
import { defineConfig } from 'vite';
export default defineConfig({
css: {
postcss: {
plugins: [scopeAngularRemoteCss({ scopeId: 'portal' })],
},
},
});
角意识插件离开外观 .component.css, (中文(简体) ), .component.scss, (中文(简体) ), .component.sass 财务报告和审定财务报表 .component.less 文件为已写入(仅包含) @keyframes 名称获得模块前缀),所以Angular完全编译了模块自己页面所编译的内容: 每个组件规则都保留其分量(mat-form-field[_ngcontent-x] 保留(0,1,1)并仍然输给一条全局规则(0,2,0)),在组件主机外的覆盖中提供模板内容保留其组件样式,以及顶级 ::ng-deep 规则保持全局到模块。 角质 _ngcontent 财务报告和审定财务报表 _nghost 属性将组件规则保存在它们的组件上,一个主机模块的组件样式活在自己的阴影根上,所以不需要边界. 组件声明 ViewEncapsulation.None 使用上面描述的全局选择器行为,因为Angular会将其 CSS 保留为全局.
在Angular AOT编译前通过 Vite/ PostCSS 运行此插件 。 别把它和 scopeRemoteCss 在同一 Vite 配置中.
东道方合同
主机通过一个 IMfShellContext 到模块的 mount 函数和预期 IMfModuleLifecycle 输出。 请检查主机版本 isSupportedMfHostContract: 它接受 MF_MINIMUM_HOST_CONTRACT_VERSION 以及以后的每一个版本,因此添加剂合同的更改不需要锁定步骤发布主机和所有模块.
import { type IMfShellContext, isSupportedMfHostContract, MF_MINIMUM_HOST_CONTRACT_VERSION } from '@edynamix/mf-isolation/host-contract';
export const mount = (element: HTMLElement, shellContext: IMfShellContext): void => {
if (!isSupportedMfHostContract(shellContext)) {
throw new Error(`This module requires host contract version ${MF_MINIMUM_HOST_CONTRACT_VERSION} or later.`);
}
// ...
};
导航表
宣布模块自主导航为有效数据并发射 navigation.json 从同一个 Vite 建筑 :
import {
defineMfNavigation,
MF_NAVIGATION_CONTRACT_VERSION,
} from '@edynamix/mf-isolation/navigation';
import { mfNavigationPlugin } from '@edynamix/mf-isolation/vite';
import { defineConfig } from 'vite';
const navigation = defineMfNavigation({
applicationId: 22,
contractVersion: MF_NAVIGATION_CONTRACT_VERSION,
items: [
{
id: 'stockmaster.for-sale',
label: 'For Sale',
order: 1,
path: 'usedstock/for-sale',
},
],
label: 'Stock Master',
landingPath: 'usedstock',
moduleId: 'stockmaster',
});
export default defineConfig({
plugins: [mfNavigationPlugin({ navigation })],
});
路线是模块相对的,没有主斜线。 该插件验证整棵树,在建构过程中用 SHA-256 修订版发出决定性的 JSON, 并服务于相同的外观 Cache-Control: no-cache 在发展中.
对于独立开发,将运算表与主机拥有的导航浏览器条目和开发BFF合并:
import {
MF_DEVELOPMENT_HOST_NAVIGATION_PATH,
mfDevelopmentBffPlugin,
} from '@edynamix/mf-isolation/vite-development';
mfDevelopmentBffPlugin({
hostNavigationUrl: 'https://host.example.test/host-navigation/index.js',
moduleApiOrigin: 'https://module.example.test',
moduleApiPath: '/stockmaster/api/',
// The sign-in service and client the module's own server accepts (its Web.config).
moduleSignIn: { issuer: 'https://login.example.test', clientId: 'module-local-client' },
});
const localHostNavigationUrl = new URL(
MF_DEVELOPMENT_HOST_NAVIGATION_PATH,
'https://localhost:4200',
).href;
hostNavigationUrl 选择主机环境。 装入浏览器条目 localHostNavigationUrl; BFF 通过安全的本地源为所选择的捆绑及其主机拥有的标志资产服务,避免在所选择的主机是HTTP本地主机实例时出现混合内容失败. 主机 API 调用到选中的主机源, 而模块 API 调用到明确 moduleApiOrigin. BFF 与 PKCE 执行 OIDC 授权代码,在开发过程中保留了令牌和模块 API cookie,并在没有暴露于模块 JavaScript的情况下转发认证的 API 请求. 主机的登录(发行者、客户端和范围) /api/auth/oidc-configuration names)授权发送到主机的一切:它的API,导航和它服务的出入口API. moduleSignIn 命名登录服务, 并客户端模块自己的服务器接受( nameissuer, (中文(简体) ), clientId,以及 scope,默认为 openid profile email offline_access);需要,插件拒绝在没有有效的情况下启动. 当它给主机的发行人和客户点名时,一个登录服务两者,就像以前一样. 否则 /api/dev-auth/login 将开发者先签入主机,再签入模块的签入服务,每个都带有自己的PKCE流和命名其步骤的状态,模块API请求携带模块的符号,而其他请求则携带主机. 每一次签入都会自动刷新,签出会同时取消两条并依次结束签入会话. 主机的刷新令牌也保存在 HttpOnly 中 __Host-edyn_dev_refresh 本地原产地的饼干, 一个单独的模块标志 -in' in __Host-edyn_dev_module_refresh,因此在 survived 服务器中签名可以重新启动,并在模块之间切换,直到一个符号过期;当模块单独签名时,重启需要两个 cookie. 登录服务接受每个刷新令牌一次,因此请求在重启的旋转饼干到达之前发送的浏览器会恢复其共享,而不是再次花出令牌并清理出新饼干. 主机API请求保留浏览器的导航选择头,由主机验证. 模块 API 请求代之以包含上次确认的主机的选择,从 /api/navigation 响应和导航流事件,通过 X-Edyn-Navigation-Group-Id, (中文(简体) ), X-Edyn-Navigation-Dealer-Ids 而对于一个庄客, X-Edyn-Navigation-Dealer-Id;模块后端在构建模块特定会话状态前仍必须授权这些值。 独立导航的门户菜单(/api/navigation/portal/...)去被选中的主机自己的门户网站 API(/portal/api/...)同样的方式,所以他们也从一个部署的宿主身上装入,这个宿主在下方没有任何转发 /api/navigation/portal。请求机构以固定长度(最多100 MB)发送,因为部署的模块前端拒绝块状机构。 在模块的第一个API调用 BFF 之前,BFF 请求主机进行当前选择,因为本地外壳在导航旁边而不是在导航之后启动模块. 一个模块的重定向到它的遗产 /login 页面在签名时引导开发签名;它显示模块 API 拒绝会话而不是在循环中重新装入模块。 当没有答案时 moduleApiOrigin,模块 API 调用答案 502 module_api_unreachable 与这个来源和原因, 终端说一次,直到模块服务器再次回答.
任意字母大小写中的角基础路径
IIS在其路径的任何字母大小写中服务一个模块,而旧的书签保留了旧的案例(/AutoPoint/...),但角质 Location 仅在精确匹配时将底线路径剥离。 安装模块 LocationStrategy 从此包中如此 /AutoPoint/... 财务报告和审定财务报表 /autopoint/... 打开模块,地址则保留它用(没有重定向)打开的大小写。 模块路径的早期名称可以作为别名相传:
import { NgModule } from '@angular/core';
import { provideCaseInsensitiveBasePath } from '@edynamix/mf-isolation/angular';
@NgModule({
providers: [...provideCaseInsensitiveBasePath(['motcleanse'])],
})
export class AppModule {}
基础是模块的 APP_BASE_HREF (或文档的) <base>); 根基(/)是未触及的。 一个将模块路径的大小写变体重定向为小写的主机,一旦模块安装了这个策略,就可以停止这样做.
策略还传递了路由器,仅传递了模块底部和别名内的地址变化. 在主机中, 返回或转发到另一个模块, 在模块卸载前更改地址; 否则模块的路由器会为它自己的地址取取一个地址, 并同时日志 NG04002 (没有匹配)或者在模块的基底下回写(一个囊括或重定向),将页面留在错误的模块中.
角 CDK 覆盖
角融合以角/CDK 19.x为对象. 在一个主机应用程序中,安装完整的提供方阵列,这样全局和连接的覆盖共享模块边界的视图和协调系统:
import { provideMfOverlayRoot } from '@edynamix/mf-isolation/angular';
const providers = provideMfOverlayRoot(overlayRoot);
全部通过 providers 通过应用的靴子。 通话 provideMfOverlayRoot() 没有元素保存了 CDK 的普通文档-视图端口行为.
通过注入符接收根的后遗症 NgModule 应用程序必须同时通过发号施令的帮助器安装两个提供者:
import { InjectionToken, NgModule } from '@angular/core';
import { provideMfOverlayRootFromToken } from '@edynamix/mf-isolation/angular';
export const MF_OVERLAY_ROOT = new InjectionToken<HTMLElement>('MF_OVERLAY_ROOT');
@NgModule({
providers: [...provideMfOverlayRootFromToken(MF_OVERLAY_ROOT)],
})
export class AppModule {}
提供运行时间 MF_OVERLAY_ROOT 通过现有平台/宝藏注入器的值。 不只提取第一个提供者 provideMfOverlayRoot(); 安装容器但省略了边界感知连接的定位.
角切入点需要 @angular/core, (中文(简体) ), @angular/common,以及 @angular/cdk 19.x (中文(简体) ).
模块之间共享角
不分享,每个主机舱会飞船,并开始自己的Angular副本. 与 createMfAngularShared() 从 @edynamix/mf-isolation/vite,同一版本的模块每页装入Angular、CDK、材料和RxJS一次:
import { federation } from '@module-federation/vite';
import { createMfAngularShared } from '@edynamix/mf-isolation/vite';
import packageJson from './package.json' with { type: 'json' };
federation({
name: 'autopoint',
filename: 'remoteEntry.js',
exposes: { './lifecycle': './src/lifecycle.ts' },
shared: createMfAngularShared(packageJson.dependencies),
});
它共享模块的每个脚本入口 @angular/* 依赖性(从每个已安装的软件包读取) exports),加 (中文(简体) ) rxjs 财务报告和审定财务报表 rxjs/operators,在模块自己的版本。 样式表也解决的条目点被忽略: @angular/material 财务报告和审定财务报表 @angular/cdk 根响应 @use '@angular/material',模块联盟将用脚本来回答样式表. @angular/compiler 财务报告和审定财务报表 @angular/platform-browser-dynamic 不共享:只有及时编译需要它们,共享它们就能将编译器放入每个模块的构建中. 版本必须准确(19.2.25没有 ^19.2.25) (中文(简体) ). 一个不同版本的模块保留了自己的副本,而不是将两个Angular版本混合在一个页面上. 过 { root } 当 Vite 配置不从模块的软件包目录运行时.
一个模块首先在页面上加载自己在启动时的每个共享切入点的副本,在批次中,联邦插件命令它们输入,一个切入点通过一个仅被加载后才被加载的根来读取另一个切入点. 因此,一个在加载时读取另一个的切入点(一个类扩展,一个静态字段,一个在模块上方的调用)需要该切入点在更早的批次中加载,插件只通过共享的包根命令它们的切入点. createMfAngularShared() 读取每个输入点的源头,并删除那些以未定义约束开头的源头(@angular/material-moment-adapter 扩展 @angular/material/core是因为 DateAdapter,但取决于未分摊 @angular/material 树根; @angular/cdk/listbox 提供 @angular/forms' NG_VALUE_ACCESSOR,而CDK并不依赖),加上每个导入它们的共享切入点,所以一个模块从不持有一个切入点的两份副本. 模块像分享之前一样,将自己的拷贝捆绑起来. mfAssetManifestPlugin() 以同样方式检查已建的输出, 如果共享的切入点仍然读取到另一个切入点, 则会失败 .
共享角是每页一个角平台. 装有 createApplication() 重新使用那个平台。 用于拖带NgModule 替换的模块 platformBrowserDynamic(providers) 与 createHostedAngularPlatform(providers)数字 :
import { createHostedAngularPlatform } from '@edynamix/mf-isolation/angular-platform';
const platform = createHostedAngularPlatform([{ provide: APP_BASE_HREF, useValue: baseHref }]);
const moduleRef = await platform.bootstrapModule(AppModule);
// on unmount
platform.destroy();
它的提供者只接触到模块本身的NgModules,以及 destroy() 只销毁它们;共享平台和其他模块继续运行。 通话 platformBrowserDynamic(providers) 而不是把模块交给另一个模块的平台 没有其供应商,及其 destroy() 将停止页面上的每一个模块。 进入点需要 @angular/platform-browser-dynamic 19.x (中文(简体) ).
东道主拥有区.js
独立的 Angular 应用程序装入自己的 Zone.js 运行时照常:
import 'zone.js';
当应用程序作为主机生命周期运行时,主机拥有单个Zone.js运行时间. 在动态导入任意角代码前检查运行时间 :
import { assertHostZoneRuntime } from '@edynamix/mf-isolation/angular-runtime';
import type { IShellContext } from './shell-context';
export const mount = async (host: HTMLElement, context: IShellContext): Promise<void> => {
assertHostZoneRuntime();
const lifecycle = await import('./lifecycle-impl');
return lifecycle.mount(host, context);
};
angular-runtime 无框架。 它证明 globalThis.Zone 是函数;它不装入或修改Zone.js.
出口
@edynamix/mf-isolation@edynamix/mf-isolation/angular@edynamix/mf-isolation/angular-runtime@edynamix/mf-isolation/angular-platform@edynamix/mf-isolation/host-contract@edynamix/mf-isolation/postcss