简单来说
从部署角度考虑 A,B,C,D 为并行四个打包后的静态文件,当有 E 应用使用 A,B,C,D 应用中的组件或者事件时通过类 eureka 服务发现注册的方式去复用组件或应用。
当然,这只是众多思路中的一种
当然,这只是众多思路中的一种
当然,这只是众多思路中的一种
好处:
应用自治: 只需要遵循统一的接口规范或者框架,以便于系统集成到一起,相互之间是不存在依赖关系的。
单一职责: 每个前端应用可以只关注于自己所需要完成的功能。
技术栈无关: 你可以使用 Angular 的同时,又可以使用 React 和 Vue。
这就好像使用 k8s 集群和 grpc 调用一样
基于基座模式的微服务无非是 服务发现
,服务注册
,服务调用
等功能
key:component|application
以应对路由不通渲染哪个应用单个浏览器多个应用还需做到 状态 |css 共享/隔离
从技术实践上,微前端架构可以采用以下的几种方式进行:
项目地址: https://github.com/worktile/ngx-planet
Ngx-Planet 是国内少有用 Angular 的公司 worktile 徐海风设计出来的一款 基于 基座模式,粒度到 Component 共用,带有消息事件注册的 微前端项目结构实现。
原帖在知乎不多赘述# 使用 Angular 打造微前端架构的 ToB 企业级应用
由于公司数十个项目都是 ng8 所以将 ngx-planet 降级为 v8 版本测试了一下,还可以
项目地址: https://github.com/ferried/ngx-planet-v8
npm install -g @angular/cli@8.3.29
ng new ngx-planet-parent --style=less
ng new portal --style=less && ng new app1 --style=less --prefix=app1
ngx-planet-parent
中 mv portal app1 ngx-planet-parent/example
ng new library ngx-planet-v8
npm install @angular/cdk@8.2.3
npm install @angular-builders/custom-webpack@8.4.1
@worktile/planet-postcss-prefixwrap@1.19.2
webpack-assets-manifest@3.1.1
library
生成的项目,将 ngx-planet项目中的 packages/ngx-planet的src
整体复制入 projects/ngx-planet-v8
最后将 mdule.ts 种的模块名称修改一下 NgxPlanetV8Module
复制tsconfig.lib.json
并打包 ng build ngx-planet-v8
example
下的两个项目加入依赖 npm install ../../dist/ngx-planet-v8
proxy.config.js
,extra-webpack.config.js
,postcss.config.js
portal 下的 postcss
module.exports = {
plugins: [require("autoprefixer")],
};
app1 下的 postcss
module.exports = {};
portal 下的 extra-webpack.config.js
,这个文件是混入 customWebpack 用的,原项目是。scss 这里改成了。less
const WebpackAssetsManifest = require("webpack-assets-manifest");
const PrefixWrap = require("@worktile/planet-postcss-prefixwrap");
module.exports = {
optimization: {
runtimeChunk: false,
},
plugins: [new WebpackAssetsManifest()],
module: {
rules: [
{
test: /\.less$/,
use: [
{
loader: "postcss-loader",
options: {
plugins: [
PrefixWrap(".portal", {
prefixRootTags: true,
}),
],
},
},
"less-loader",
],
},
],
},
};
app1 下的 extra-webpack.config.js
,
const WebpackAssetsManifest = require("webpack-assets-manifest");
const PrefixWrap = require("@worktile/planet-postcss-prefixwrap");
module.exports = {
optimization: {
runtimeChunk: false,
},
plugins: [new WebpackAssetsManifest()],
module: {
rules: [
{
test: /\.less$/,
use: [
{
loader: "postcss-loader",
options: {
plugins: [
PrefixWrap(".app1", {
hasAttribute: "planet-inline",
prefixRootTags: true,
}),
],
},
},
"less-loader",
],
},
],
},
};
portal 下的 proxy.config.js
const PROXY_CONFIG = {};
PROXY_CONFIG["/static/app1"] = {
target: "http://localhost:3001",
secure: false,
changeOrigin: false,
};
PROXY_CONFIG["/static/app2"] = {
target: "http://localhost:3002",
secure: false,
changeOrigin: true,
};
module.exports = PROXY_CONFIG;
portal 的 angular.json
{
...
"architect": {
"build": {
// build改成 custom-webpack
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
// extra-webpack.config.js
"path": "extra-webpack.config.js",
// 混入配置
"mergeStrategies": {
"externals": "replace",
"module.rules": "append"
}
},
// baseHref
"baseHref": "/",
"outputPath": "dist/portal",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
// 加入 extractCss
"extractCss": true,
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.less"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
// 加入 vendorChunk
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
// serve改成 custom-webpack
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"browserTarget": "portal:build",
// 混入代理文件
"proxyConfig": "proxy.conf.js",
"port": 3000
},
"configurations": {
"production": {
"browserTarget": "portal:build:production"
}
}
},
...
"defaultProject": "portal"
}
app1 的 angular.json
{
...
"architect": {
"build": {
// 同样改成custom-webpack:browser
"builder": "@angular-builders/custom-webpack:browser",
"options": {
// 混入配置
"customWebpackConfig": {
"path": "extra-webpack.config.js",
"mergeStrategies": {
"module.rules": "append"
},
// replaceDuplicatePlugins
"replaceDuplicatePlugins": true
},
"outputPath": "dist/app1",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
// vendorChunk & extractCss
"vendorChunk": false,
"extractCss": true,
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.less"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
// extractCss && vendorChunk
"extractCss": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
// custom-webpack:dev-server,加入port3001和vendorChunk
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"port": 3001,
"vendorChunk": false,
"browserTarget": "app1:build"
},
...
"defaultProject": "app1"
}
修改 Approuting
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { EmptyComponent } from "ngx-planet-v8";
const routes: Routes = [
{
path: "app1",
component: EmptyComponent,
children: [
{
path: "**",
component: EmptyComponent,
},
],
},
{
path: "app2",
component: EmptyComponent,
children: [
{
path: "**",
component: EmptyComponent,
},
],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
app.module
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { FormsModule } from "@angular/forms";
import { CommonModule } from "@angular/common";
import { NgxPlanetV8Module } from "ngx-planet-v8";
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
CommonModule,
AppRoutingModule,
// 引入模块
NgxPlanetV8Module,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
app.component.html
// 通过路由从基座应用跳到app1
<a [routerLink]="['/app1']" routerLinkActive="active"> APP1 </a>
// 容器
<div id="app-host-container">
<router-outlet></router-outlet>
</div>
// 加载状态
<div *ngIf="!loadingDone">加载中</div>
app.module.ts
import { Component, OnInit } from "@angular/core";
import { Planet, SwitchModes } from "ngx-planet-v8";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.less"],
})
export class AppComponent implements OnInit {
title = "portal";
get loadingDone() {
return this.planet.loadingDone;
}
constructor(private planet: Planet) {}
ngOnInit(): void {
// 向基座注册app1应用,当然可以变成json通过http远程配置
const appHostClass = "thy-layout";
this.planet.registerApps([
{
name: "app1",
hostParent: "#app-host-container",
hostClass: appHostClass,
routerPathPrefix: /\/app1|app4/, // '/app1',
resourcePathPrefix: "/static/app1/",
preload: false,
switchMode: SwitchModes.coexist,
loadSerial: true,
stylePrefix: "app1",
// prettier-ignore
scripts: [
'main.js',
// 'polyfills.js'
],
styles: ["styles.css"],
manifest: "/static/app1/manifest.json",
extra: {
name: "应用1",
color: "#ffa415",
},
},
{
name: "app2",
hostParent: "#app-host-container",
hostClass: appHostClass,
routerPathPrefix: "/app2",
resourcePathPrefix: "/static/app2/",
preload: false,
switchMode: SwitchModes.coexist,
stylePrefix: "app2",
// prettier-ignore
scripts: [
'main.js'
],
styles: ["styles.css"],
manifest: "/static/app2/manifest.json",
extra: {
name: "应用2",
color: "#66c060",
},
},
]);
this.planet.start();
}
}
main.ts
import { enableProdMode, NgModuleRef, Type, NgZone } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
import { environment } from "./environments/environment";
import { PlanetPortalApplication, defineApplication } from "ngx-planet-v8";
if (environment.production) {
enableProdMode();
}
defineApplication("app1", {
template: `<app1-root class="app1-root"></app1-root>`,
bootstrap: (portalApp: PlanetPortalApplication) => {
return platformBrowserDynamic([
{
provide: PlanetPortalApplication,
useValue: portalApp,
},
])
.bootstrapModule(AppModule)
.then((appModule) => {
return appModule;
})
.catch((error) => {
console.error(error);
return null;
});
},
});
routing.ts
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { EmptyComponent } from "ngx-planet-v8";
const routes: Routes = [{ path: "app1", component: EmptyComponent }];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
module.ts
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";
import { FormsModule } from "@angular/forms";
import { NgxPlanetV8Module } from "ngx-planet-v8";
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
CommonModule,
AppRoutingModule,
RouterModule,
NgxPlanetV8Module,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
最后向 app1 的 package.json 中 script
下加入 "start": "ng serve --deploy-url=/static/app1/",
这个 deploy-url 将来就是你 nginx 的文件夹路径
end: 分别启动 portal 和 app1 然后进行测试吧