微前端将web应用由单一的单体应用转变为多个小型前端应用聚合为一的应用,每个前端应用还可以独立运行、独立开发、独立部署。同时,他们也可以在共享组件的同时进行并行开发–这些组件可以通过Git Tag等来管理。
使用微前端框架对旧有的服务运行,而使用新框架构建新服务是最好的尝试。
微前端构建的应用是前后端分离的单页面应用,在此基础上微前端才有意义。
微前端架构一般分为以下路线:
在形式上说,单体前端框架的路由和单体后端应用没有太大区别:依据不同的路由,来返回不同页面的模版
原先单页面应用的路由配置:
const appRoutes: Routes = [
{ path: 'index', component: IndexComponent },
{ path: 'detail/:id', component: DetailComponent },
];
当我们将之微服务后,应用A的路由:
const appRoutes: Routes = [
{ path: 'index', component: IndexComponent },
];
以及应用B的路由:
const appRoutes: Routes = [
{ path: 'detail/:id', component: DetailComponent },
];
使用HTTP服务器的反向代理或应用框架自带的路由,通过路由将不同的业务分发到不同的、独立前端应用上。
这种方式每跳转到一个应用,相当于刷新一次页面
如下是一个基于路由分发的Nginx配置:
http {
server {
listen 80;
server_name www.phodal.com;
location /api/ {
proxy_pass http://http://172.31.25.15:8000/api;
}
location /web/admin {
proxy_pass http://172.31.25.29/web/admin;
}
location /web/notifications {
proxy_pass http://172.31.25.27/web/notifications;
}
location / {
proxy_pass /;
}
}
}
采用iFrame可以创建一个全新的独立的宿主环境,有效地将另一个HTML页面嵌入到当前页面中
采用iFrame有两个重要的前提:
不论是基于 Web Components 的 Angular,或者是 VirtualDOM 的 React 等,现有的前端框架都离不开基本的 HTML 元素 DOM
那么,我们只需要:
第2个问题关键在移除DOM和相应应用的监听。
尽管如react等Single-Page框架已经拥有启动和卸载处理,但是它仍然不适应生产用途,需要重写一个框架,如Mooa
组合式集成,即通过软件工程的方式在构建前、构建时、构建后等步骤中,对应用进行一步的拆分,并重新组合。
从这种定义上来看,它可能算不上并不是一种微前端——它可以满足了微前端的三个要素,即:独立运行、独立开发、独立部署。但是,配合上前端框架的组件 Lazyload 功能——即在需要的时候,才加载对应的业务组件或应用,它看上去就是一个微前端应用。
但是,首先它有一个严重的限制:必须使用同一个框架。对于多数团队来说,这并不是问题。采用微服务的团队里,也不会因为微服务这一个前端,来使用不同的语言和技术来开发。当然了,如果要使用别的框架,也不是问题,我们只需要结合上一步中的自制框架兼容应用就可以满足我们的需求。
采用这种方式由一些限制,就是规范,我们需要:
Web Components 是一套不同的技术,允许构建可重用的定制元素(它们封装在您的代码之外)并且在您的web应用中使用它们,它主要由四项技术组件:
和
元素,用于编写不在页面中显示的标记模版。每个组件由link
标签引入:
<link rel="import" href="components/di-li.html">
<link rel="import" href="components/d-header.html">
随后,在各自的HTML文件中,构建相应的组件元素,编写相应的组件逻辑。
基于qiankun,先在主应用中创建微应用的承载容器,这个容器规定了微应用的显示区域,微应用将在该容器内渲染并显示。
以Vue为基座,路由文件规定了主应用自身的路由匹配规则:
// micro-app-main/src/routes/index.ts
import Home from "@/pages/home/index.vue";
const routes = [
{
/**
* path: 路径为 / 时触发该路由规则
* name: 路由的 name 为 Home
* component: 触发路由时加载 `Home` 组件
*/
path: "/",
name: "Home",
component: Home,
},
];
export default routes;
// micro-app-main/src/main.ts
//...
import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes";
/**
* 注册路由实例
* 即将开始监听 location 变化,触发路由规则
*/
const router = new VueRouter({
mode: "history",
routes,
});
// 创建 Vue 实例
// 该实例将挂载/渲染在 id 为 main-app 的节点上
new Vue({
router,
render: (h) => h(App),
}).$mount("#main-app");
构建好主框架后,使用qiankun的registerMicroApps
方法注册微应用,如下:
// micro-app-main/src/micro/apps.ts
// 此时我们还没有微应用,所以 apps 为空
const apps = [];
export default apps;
// micro-app-main/src/micro/index.ts
// 一个进度条插件
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { message } from "ant-design-vue";
import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
start,
} from "qiankun";
// 微应用注册信息
import apps from "./apps";
/**
* 注册微应用
* 第一个参数 - 微应用的注册信息
* 第二个参数 - 全局生命周期钩子
*/
registerMicroApps(apps, {
// qiankun 生命周期钩子 - 微应用加载前
beforeLoad: (app: any) => {
// 加载微应用前,加载进度条
NProgress.start();
console.log("before load", app.name);
return Promise.resolve();
},
// qiankun 生命周期钩子 - 微应用挂载后
afterMount: (app: any) => {
// 加载微应用前,进度条加载完成
NProgress.done();
console.log("after mount", app.name);
return Promise.resolve();
},
});
/**
* 添加全局的未捕获异常处理器
*/
addGlobalUncaughtErrorHandler((event: Event | string) => {
console.error(event);
const { message: msg } = event as any;
// 加载失败时提示
if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
message.error("微应用加载失败,请检查应用是否可运行");
}
});
// 导出 qiankun 的启动函数
export default start;
微应用注册信息在apps
数组中,然后使用registerMicroApps
方法注册微应用,最后导出start函数
以react为例子,注册微应用
首先,在主应用中注册微应用的信息,如下:
// micro-app-main/src/micro/apps.ts
const apps = [
/**
* name: 微应用名称 - 具有唯一性
* entry: 微应用入口 - 通过该地址加载微应用
* container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
*/
{
name: "ReactMicroApp",
entry: "//localhost:10100",
container: "#frame",
activeRule: "/react",
},
];
export default apps;
在主应用中注册了我们的react微应用,进入/react
路由时加载我们的react微应用
在菜单配置处也加入react微应用的快捷入口,如中后台中子菜单选项启动微应用
// micro-app-main/src/App.vue
//...
export default class App extends Vue {
/**
* 菜单列表
* key: 唯一 Key 值
* title: 菜单标题
* path: 菜单对应的路径
*/
menus = [
{
key: "Home",
title: "主页",
path: "/",
},
{
key: "ReactMicroApp",
title: "React 主页",
path: "/react",
},
{
key: "ReactMicroAppList",
title: "React 列表页",
path: "/react/list",
},
];
}
随后,在react的入口文件index.js
中,导出qiankun主应用所需的三个生命周期钩子函数:
// micro-app-react/src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 webpack publicPath,防止资源加载出错
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// micro-app-react/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./public-path";
import App from "./App.jsx";
/**
* 渲染函数
* 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
*/
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log("ReactMicroApp bootstraped");
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log("ReactMicroApp mount", props);
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log("ReactMicroApp unmount");
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
最后,新建config.overrides.js
文件来配置webpack
const path = require("path");
module.exports = {
webpack: (config) => {
// 微应用的包名,这里与主应用中注册的微应用名称一致
config.output.library = `ReactMicroApp`;
// 将你的 library 暴露为所有的模块定义下都可运行的方式
config.output.libraryTarget = "umd";
// 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
config.output.jsonpFunction = `webpackJsonp_ReactMicroApp`;
config.resolve.alias = {
...config.resolve.alias,
"@": path.resolve(__dirname, "src"),
};
return config;
},
devServer: function (configFunction) {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
// 关闭主机检查,使微应用可以被 fetch
config.disableHostCheck = true;
// 配置跨域请求头,解决开发环境的跨域问题
config.headers = {
"Access-Control-Allow-Origin": "*",
};
// 配置 history 模式
config.historyApiFallback = true;
return config;
};
},
};