微前端-从了解到动手搭建
前言
微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署。
为什么出现?
与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和业务逻辑也愈发难以维护,尤其对于中后台系统,很容易出现巨石应用,微前端由此应运而生,其根本目的就是解决巨石应用的项目复杂,系统庞大,开发人员众多,难以维护的问题。
微前端 vs 巨石应用
微前端 | 巨石应用 | |
---|---|---|
可维护性 | 拆分为框架应用、微应用、微模块后,每个业务页面都对应一个单独的仓库,应用风险性降低。 | 所有页面都在一个仓库,经常会出现动一处则动全身,随着系统增大维护成本会逐渐升高。 |
开发效率 | 结合发布、回滚、团队协作三个方面来看,单个仓库只关心一个业务页面,可以更方便快速迭代。 | 团队多人协作时,发布排队;回滚有可能会把其他人发布的代码同时回滚掉;多分支开发时发布前沟通增加成本。 |
代码复用 | 所有页面都分开维护,使用公用代码成本较大,不过共用代码抽离为npm包使用可以减小成本。 | 一个仓库中很容易抽离公用的部分,但是要注意动一处就会动全身的结果。 |
架构方案
基座模式是当前比较常见的微前端架构设计。
首先以容器应用作为整个项目的主应用,负责子应用的注册,聚合,提供子运行环境、管理生命周期等。子应用就是各个独立部署、独立开发的单元。
应用注册表拥有每个应用及对应的入口。在前端领域里,入口的直接表现形式可以是路由,又或者对应的应用映射。
目前可以实现微前端架构的方案有如下:
HTTP后端路由转发(nginx)
✅ 简单高效快速,同时不需要前端做额外的工作。
❌ 体验并不好,相当于mpa页面,路由到每个应用需要重新刷新
iframe
✅ 前端最简单的应用方式,直接嵌入,门槛最低,改动最小
❌ iframe都会遇到的一些典型问题:UI 不同步,DOM 结构不共享(比如iframe中的弹框),跨域通信等
各个业务独立打到npm包中
✅ 门槛低,易上手
❌ 模块修改后需要重新部署发布,太麻烦。
组合式应用路由分发(基座模式)
✅ 纯前端改造,体验良好,各个业务相互独立
❌ 需要设计和开发,有一定成本,同时需要兼顾子页面和基座的变量污染,样式互相影响等问题
web component
✅ 是一项标准,目前它包含三项主要技术,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。应该是微前端的最终态
❌ 比较新,兼容性较差
微前端页面形态
微前端基座框架需要解决的问题
路由分发
作为微前端的基座应用,是整个应用的入口,负责承载当前子应用的展示和对其他路由子应用的转发,对于当前子应用的展示,一般是由以下几步构成:
远程拉取子应用内容
将子应用的 js 和 css 抽离,采用eval来运行 js,并将 css 和 html 内容append到基座应用中留给子应用的展示区域
当子应用切换走时,同步卸载这些内容
对于路由分发而言,以采用react-router开发的基座SPA应用来举例,主要是下面这个流程:
当浏览器的路径变化后,react-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。
最先接收到这个变化的是基座的router,通过查询注册信息可以获取到转发到那个子应用,经过一些逻辑处理后,采用修改hash方法或者pushState方法来路由信息推送给子应用的路由,子应用可以是手动监听hashchange或者popstate事件接收,或者采用react-router接管路由,后面的逻辑就由子应用自己控制。
应用隔离
应用隔离问题主要分为主应用和子应用,子应用和子应用之间的JavaScript执行环境隔离,CSS样式隔离,
CSS
当主应用和子应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个子应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。
而对于子应用与子应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。
JavaScript隔离
每当子应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个window.$对象,对于其他库React,Vue也不例外。为此,需要在加载和卸载每个子应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。
沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象,需要结合 with 关键字和window.Proxy对象来实现浏览器端的沙箱。
消息通信
应用间通信有很多种方式,当然,要让多个分离的子应用之间要做到通信,本质上仍离不开中间媒介或者说全局对象。所以对于消息订阅(pub/sub)模式的通信机制是非常适用的,在基座应用中会定义事件中心Event,每个子应用分别来注册事件,当被触发事件时再有事件中心统一分发,这就构成了基本的通信机制。
当然,如果基座和子应用采用的是React或者是Vue,是可以结合Redux和Vuex来一起使用,实现应用之间的通信。
搭一个看看?
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。也是支付宝内部广泛使用的微前端框架。
那么我们就使用 qiankun 从头搭一个demo出来体验一下
基座
基座我们使用react,自行使用 create-react-app 创建一个react项目即可。
npm install qiankun -s
在基座中需要调用 registerMicroApps 注册子应用,然后调用start启动
因此在 index.js 中插入如下代码
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
]);
// 启动 qiankun
start();
修改App.js
加入一些 antd 元素,让demo像样一些
同时,由于qiankun根据路由来加载不同微应用,我们也安装 react-router-dom
npm install react-router-dom
安装完之后修改 App.js 如下:
import { useState } from 'react';
import { Layout, Menu } from 'antd';
import { PieChartOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom'
import './App.css';
const { Header, Content, Footer, Sider } = Layout;
const App = () => {
const [collapsed, setCollapsed] = useState(false);
const onCollapse = collapsed => {
setCollapsed(collapsed);
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse}>
<div className="logo" />
<Menu theme="dark" defaultSelectedKeys={['1']} mode="inline">
<Menu.Item key="1" icon={<PieChartOutlined />}>
<Link to="/app-vue">Vue应用</Link>
</Menu.Item>
</Menu>
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background" style={{ padding: 0 }} />
<Content style={{ margin: '16px' }}>
<div id="container" className="site-layout-background" style={{ minHeight: 360 }}></div>
</Content>
<Footer style={{ textAlign: 'center' }}>This Project ©2021 Created by DiDi</Footer>
</Layout>
</Layout>
);
}
export default App;
记得修改 index.js,把 App 组件用 react-router-dom 的 BrowserRouter 包一层,让 BrowserRouter 作为顶层组件才可以跳转
至此,基座搭好了
子页面
尝试使用vue作为子页面,来体现微前端的技术隔离性。
使用vue-cli创建vue2.x项目
修改main.js如下:
import Vue from "vue/dist/vue.js";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
// window.__POWERED_BY_QIANKUN__ 为true 说明在 qiankun 架构中
// 修改webpack的publicPath,将子应用资源加载的公共基础路径设为 qiankun 包装后的路径
// 这个 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 的实际地址是子应用的服务器地址,子应用的应用资源都在他本身的实际服务器上
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时 直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 应用需要导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
export async function bootstrap() {
console.log("[vue] vue app bootstraped");
}
export async function mount(props) {
console.log("[vue] props from main framework", props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}
router.js配置如下:
import Vue from "vue/dist/vue.js";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const routes = [
{
path: "/test",
name: "Test",
component: () => import("./components/Test.vue"),
},
{
path: "/hello",
name: "Hello",
component: () => import("./components/Hello.vue"),
},
];
const router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? "/app-vue/" : "/",
mode: "history",
routes,
});
export default router;
根目录下新建vue.config.js 用来配置webpack,内容如下:
const { name } = require("./package");
module.exports = {
devServer: {
// 跨域
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
// 把微应用打包成 umd 库格式
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
启动
基座和子应用分别启动,可以看到,子应用已经加载到了主应用中: