注册

微前端-从了解到动手搭建

前言

微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署

为什么出现?

与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和业务逻辑也愈发难以维护,尤其对于中后台系统,很容易出现巨石应用,微前端由此应运而生,其根本目的就是解决巨石应用的项目复杂,系统庞大,开发人员众多,难以维护的问题。

微前端 vs 巨石应用


微前端巨石应用
可维护性拆分为框架应用、微应用、微模块后,每个业务页面都对应一个单独的仓库,应用风险性降低。所有页面都在一个仓库,经常会出现动一处则动全身,随着系统增大维护成本会逐渐升高。
开发效率结合发布、回滚、团队协作三个方面来看,单个仓库只关心一个业务页面,可以更方便快速迭代。团队多人协作时,发布排队;回滚有可能会把其他人发布的代码同时回滚掉;多分支开发时发布前沟通增加成本。
代码复用所有页面都分开维护,使用公用代码成本较大,不过共用代码抽离为npm包使用可以减小成本。一个仓库中很容易抽离公用的部分,但是要注意动一处就会动全身的结果。

架构方案

基座模式是当前比较常见的微前端架构设计。

首先以容器应用作为整个项目的主应用,负责子应用的注册,聚合,提供子运行环境、管理生命周期等。子应用就是各个独立部署、独立开发的单元。

202112032216614.png应用注册表拥有每个应用及对应的入口。在前端领域里,入口的直接表现形式可以是路由,又或者对应的应用映射。

目前可以实现微前端架构的方案有如下:

HTTP后端路由转发(nginx)

  • ✅ 简单高效快速,同时不需要前端做额外的工作。

  • ❌ 体验并不好,相当于mpa页面,路由到每个应用需要重新刷新

iframe

  • ✅ 前端最简单的应用方式,直接嵌入,门槛最低,改动最小

  • ❌ iframe都会遇到的一些典型问题:UI 不同步,DOM 结构不共享(比如iframe中的弹框),跨域通信等

各个业务独立打到npm包中

  • ✅ 门槛低,易上手

  • ❌ 模块修改后需要重新部署发布,太麻烦。

组合式应用路由分发(基座模式)

  • ✅ 纯前端改造,体验良好,各个业务相互独立

  • ❌ 需要设计和开发,有一定成本,同时需要兼顾子页面和基座的变量污染,样式互相影响等问题

web component

  • ✅ 是一项标准,目前它包含三项主要技术,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。应该是微前端的最终态

  • ❌ 比较新,兼容性较差

微前端页面形态

202112032213206.png

微前端基座框架需要解决的问题

路由分发

作为微前端的基座应用,是整个应用的入口,负责承载当前子应用的展示和对其他路由子应用的转发,对于当前子应用的展示,一般是由以下几步构成:

  1. 远程拉取子应用内容

  2. 将子应用的 js 和 css 抽离,采用eval来运行 js,并将 css 和 html 内容append到基座应用中留给子应用的展示区域

  3. 当子应用切换走时,同步卸载这些内容

对于路由分发而言,以采用react-router开发的基座SPA应用来举例,主要是下面这个流程:

  1. 当浏览器的路径变化后,react-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。

  2. 最先接收到这个变化的是基座的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}`,
  },
},
};

启动

基座和子应用分别启动,可以看到,子应用已经加载到了主应用中:

202112032211386.png
作者:visa
来源:https://juejin.cn/post/7037386845751083021

0 个评论

要回复文章请先登录注册