前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微前端的落地和治理实战

微前端的落地和治理实战

作者头像
_sx_
发布2023-10-20 15:55:09
5661
发布2023-10-20 15:55:09
举报
文章被收录于专栏:前端技术地图

微前端实现原理、框架选型之类的文章比较泛滥,我不打算讲这些玩意,本文主要来源于笔者过去一年落地微前端的一手经验,尽量不讲技术细节,而是讲一个体系化的方案是怎么搭建起来。

文章较长,耐心看完保证会有收获。

背景与痛点

首先来看下业务背景,方便读者了解我们为什么选择微前端,以及其他相关技术选型的原因。

前端在架构上面的变化远落后于后端,后端的架构已经经历了微服务、中台化、DDD 改造的腥风血雨…

在改造成微前端之前, 我们也是一个巨型的单体应用,后面随着业务的复杂化,业务和团队进一步进行拆分, 我们的前端项目也根据康威定律,进化成为了‘多页应用’, 如下图所示:

我们主要做的是 2B 业务,做 POC(概念验证) 和私有化部署是家常便饭,在已有的架构下,我们需要应用某些配置可能会牵扯多个项目,比如主题、文案、接口配置等信息的修改,需要针对多个项目进行创建分支、修改代码、构建、发布、部署… 一系列繁琐的流程

主要原因是我们的业务系统经过长期、多团队、多业态的迭代,积累了大量的技术债。

  • 技术栈老旧,开发效率低,我们想要应用新的技术和规范,但碍于项目体量大、质量差,重构举步维艰。
  • 子应用的拆分没有固定的范式。有些模块按照团队拆分出独立的仓库,有些仓库则采用 MonoRepo。前者仓库之间存在大量重复代码、缺乏管理;而后者 MonoRepo 则越来越臃肿, 职责不清晰,编译缓慢, 逐渐也演变成了巨石应用
  • 基于多页的子应用缺乏管理,规范/标准不统一。无法统一控制视觉呈现、共享的功能和依赖。造成重复工作
  • 新旧项目、第三方应用集成都很复杂。
  • 多行业、多团队的项目特性,导致工程管理复杂,扩展性差。
  • 部署方式原始。
  • 应用按照菜单聚合,而不是按照业务聚合

💡 怎么理解 “应用按照菜单聚合,而不是按照业务聚合” 呢?

朴素的多页应用通常按照“菜单”来拆分应用,比如按照上图的顶级 Tab。 后面来这一个这样的需求,a 应用的某些功能菜单需要在 b Tab 下展示,这时候就傻眼了:

  • 把 a 的相关代码搬运到 b?如果后面菜单又改了怎么办?再说,你能搬得动吗?
  • 用 iframe 将 a 套在 b 应用下?

因此我们亟需一套新的架构,能统一管理不同团队业务线、同时能够保持原本的独立性和灵活性。这时候微前端架构就进入了我们的考察范围:

我们需要一个「底座」将不同的应用聚合起来,将原本离散的应用通过一个基座串联起来:

  • 离散的应用结构,转换为星状结构。基座可以统一管理子应用。
  • 开发者可以更专注于业务的开发。基座会提供配套的登录会话管理、权限管理、菜单管理、路由管理、主题管理等方案,子应用只需关心业务功能本身的开发。
  • 更容易地集成应用。不管是自己的业务应用、老旧系统、还是外部第三方应用,都可以在极少改动的情况下集成进来。
  • 视觉统一
  • 拆分巨石应用,让子应用可以按照“业务聚合”不再耦合菜单, 让应用更轻量、内聚、更可维护

💡 使用微前端之后,子应用不再耦合菜单,菜单由基座来管理和组合,菜单可以被放在任意位置。

架构

由于我们原本就是多页应用的架构,所以基于路由分发 + 基座形式的微前端方案是一种比较自然的选择。整体项目架构如下:

我们构造了一整套体系化的方案: 从规范开发基础库、从权限管理系统微前端基座、从开发调试部署运维

  1. 基础库:我们将每个应用都重复的工作提取出来,重新设计,并严格管理起来。使之能真正有效地服务业务开发,避免重复造轮子。
  2. 开发规范:同时,我们期望提供更丰富的开发规范、指导、最佳实践作为支撑。让开发者走更少的弯路。
  3. 权限管理平台:基座的菜单权限信息来源于权限管理平台, 通过权限管理平台可以灵活地给不同业态、不同角色配置不同的菜单和权限。这是我们微前端方案的重要基础。
  4. 基座: 基座是微前端应用集成的一个重要平台。同时也肩负着管理公共资源、依赖、规范的责任。主要有以下职责:
    • 子应用集成。给子应用提供渲染容器
    • 路由/菜单管理
    • 权限管理
    • 主题管理
    • 会话管理
    • 多语言管理
    • 共享依赖等
  5. 运行容器:运行容器 是我们提供的一套微前端的运行和部署方案。相比传统纯粹的前端资源静态部署,我们希望在部署阶段可以做更多的事情:
    • 动态配置。比如域名配置、SEO 信息配置
    • 主题管理。一键换肤能否实现?
    • 子应用管理。自动发现子应用,而不是在微前端基座中硬编码?
    • 语言包。能否实时配置语言包,而不需要重新编译代码、审核、发布…
    • 开发环境、测试环境部署能否简化?

    得益于运行容器,我们可以实现前端部署的标准化,支持「一键部署」等能力。

基座

基座主界面

如上所示,基座为子应用提供了基础的运行环境, 蓝色区域为子应用的运行范围。

基座的大概结构如上。

首先是会话管理,基座会拦截应用的所有请求,如果监听到 401 状态码,则跳转到登录页面进行授权。登录/注册页面也是由子应用提供,我们尽量不让基座耦合具体的业务。

基座启动后,就会从权限管理平台拉取菜单、权限配置信息,渲染页面的菜单导航框架。同时也会对页面路由进行授权拦截,而细粒度的权限控制(比如按钮),基座也会暴露 API 供子应用适配。

至于子应用信息,则是运行容器自动发现并注入,避免在基座中硬编码了这些信息。基座底层基于 qiankun,根据路由匹配渲染指定的子应用。

💡 运行容器是啥? 这个我们会在下文介绍,简单来说,它就是一个 NodeJS 服务,会自动发现已经部署在服务器中的子应用,然后将这些信息注入到基座的启动代码中。

基座还统一管理了主题包多语言。从而保证子应用可以有较为统一的呈现。主题包也可以在部署时动态切换,这对于 POC 或者私有化部署比较方便。

💡 主题包主要包含 CSS 变量、组件库样式、语言包、静态资源、甚至一些部署配置信息。

为了方便子应用使用基座的「服务」, 基座也向子应用暴露了一系列的组件库和 API。

组件库基于使用 Web Component 的形式,实现框架无关, 基于 Vue 3 创建。Vue 3 构建自定义元素 也很方便,所以就没必要引入其他框架专门来编写这块了

这些 API 可以直接挂载在全局 window 对象上,子应用可以直接访问。

💡 实际上我们封装了一个套壳 npm 库,避免子应用直接访问 window 对象上的服务, 隐藏细节,另外可以提供类型提示。

子应用接入

简单、免侵入地改造子应用使我们要达成的主要目标。

配置

为此,我们也提供了相应的 vue-cli 插件, 支持快速集成,避免开发者关心 Webpack 底层的各种配置细节

我们的微前端主要基于 qiankun,官方目前并不支持 Vite,并且我们大量项目主要以 Vue CLI 为主。

示例:

代码语言:javascript
复制
const { defineConfig } = require('@vue/cli-service')
const { defineMappChild } = require('@wakeadmin/vue-cli-plugin-mapp-child')

module.exports = defineConfig({
  transpileDependencies: false,
  pluginOptions: {
    // 只需要简单的配置
    ...defineMappChild({
      mapp: {
        activeRule: '/dsp.html',
      },
    }),
  },
  lintOnSave: false,
})

多入口配置:

代码语言:javascript
复制
const { defineConfig } = require('@vue/cli-service')
const { defineMappChild } = require('@wakeadmin/vue-cli-plugin-mapp-child')

module.exports = defineConfig({
  // 多页应用
  pages: {
    index: 'src/main.ts',
    another: 'src/another.ts',
  },
  pluginOptions: {
    // 微前端集成配置
    ...defineMappChild({
      mapp: [
        {
          // entry 必须为上面 pages 中定义的 key
          entry: 'index',
        },
        {
          // entry 必须为上面 pages 中定义的 key
          entry: 'another',
        },
      ],
    }),
  },
})

挂载

接着调整应用挂载程序:

代码语言:javascript
复制
import { createApp, App as TApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import Bay from '@wakeadmin/bay'

import App from './App.vue'
import { routes } from './router'
import store from './store'

let app: TApp

Bay.createMicroApp({
  async bootstrap() {
    console.log('bootstrap vue3')
  },
  async mount(container, props) {
    console.log('mount vue3', props)

    const router = createRouter({
      history: createWebHashHistory(),
      routes,
    })

    app = createApp(App).use(store).use(router).use(Bay)

    app.mount(container?.querySelector('#app') ?? '#app')
  },

  async unmount() {
    console.log('unmount vue3')

    app.unmount()
  },

  async update() {
    console.log('update vue3')
  },
})

本地开发和调试

运行起来后, 我们会在终端打印出子应用的相关信息,如下图:

接下来,只需要在基座的调试页面,注册这个子应用就可以运行起来的:

💡 有了微前端之后,子应用的开发和调试也简化了很多,可以随时挂载到任意环境,不需要配置任何服务端代理。

部署和治理

网上很少关于微前端应用的部署和治理的介绍,下面介绍我们自己摸索出来一套方案, 这也是本文的重点。

容器化

在此之前,我们的前端项目都是扔到一台静态资源服务器,很多开发者会手动操作,项目之前通过目录隔离,手动维护 Nginx 进行分流,手段原始且容易出错,场面十分混乱。

在 2021 年,我们就开始推行前端项目的容器化,来解决这种混乱的状态。

为了标准化自动化每个项目的构建操作、部署流程,我们和后端对齐, 使用容器和 K8S 来实现发布产物的封装和部署。这样的好处是:

  • 实现测试环境和生产环境的统一。
  • 简化部署流程, 采用统一的配置,无需更改 Nginx 配置
  • 真正做到不同团队项目的隔离。
  • 支持回滚
  • 简化和标准化构建流程。同时也简化了运维的工作,前后端都是容器部署。
  • 运行的环境更加灵活。我们可以使用最新的 nginx 版本,可以使用 HTTP2 等新的技术,前端自己就可以部署一套 NodeJS 环境,做一些更酷的事情。对运维的依赖性会更低。

这对我们来说是一个比较重要的升级。我们的工作不再局限于静态资源的伺服,我们可以使用 NodeJS 开发 API、自动化工作流、可以进行服务端渲染等等,拓展了能力的边界。

然而,很多配置信息在构建时就固定下来了,比如 CDN 域名,接口请求路径等等。而不同环境通常会使用不同的配置信息。这样就无法实现构建一次镜像,在不同环境运行。

后端程序的解决办法是将配置信息外置,比如通过环境变量配置或者从配置中心(比如 Nacos)获取。

这在前端行不通,所以我们引入了运行容器的概念。

运行容器

运行容器,顾名思义就是整套微前端运行时,以 「Docker 容器」的形式部署。我们尽量复用 K8S 提供的基础设施(比如 PVC、配置映射、Sidecar 等) 来实现。

运行容器的主要结构:

运行容器主要包含两大部分:

  • Nginx :毫无疑问,Nginx 是静态资源伺服的最佳能手,同时它作为内部服务反向代理
  • transpiler (我们称为转换器): 这是一个「搬运工」,主要负责配置的收集、代码转换。并将转换后的静态资源交给 nginx 伺服。

下面会详细介绍它的能力。

如果实现子应用的自动发现?

答案是”约定“

运行容器约定了以下目录:

代码语言:javascript
复制
/data/
  /source/                   # 源目录
    /__public__/             # 公共资源, 外部可以直接访问,不需要 __public__ 前缀
    /__config__/             # 配置目录
      config.yml
      any-sub-dir/
        my-config.yml

    /__entry__/              # 基座目录
      js/
      index.html

    /__apps__/               # 子应用目录
      wkb/
      dsp/
      dmp/
        js/
        mapp.json
        index.html

    /__i18n__/               # 语言包目录
      zh.tr
      en.tr
      any-sub-dir/
        zh.tr
        en.tr

    /__theme__/              # 主题目录
      config.yml
      element-ui.css
      element-plus.css
      fonts/
      i18n/
        zh.tr
        en.tr

  /public/                    # nginx 伺服目录

目录结构解析:

  • /data/source。没错,transpiler 就是转译和搬运这里的静态资源。
  • /data/publictranspiler 就是将资源转译后搬运到这里,nginx 对外伺服这个目录。

转译?transpiler 可以认为就是一个模板引擎,它会替换代码里面的动态变量。

再来看 /data/source

__entry__: 基座编译之后的代码就部署这里。

__apps__: 子应用编译之后的代码就部署这里,子应用之间, 按照唯一的 name 区分目录。

__i18n__: 扩展语言包,文件按照 <language>.tr 命名, 子目录的 .tr 文件也会被扫描到。

__config__: 配置目录。配置文件使用 .yml.yaml 命名,也可以放在子目录下。

__theme__: 主题包目录。可以手动维护,也可以使用 npmTheme 配置项, 让 transpiler 从 npm 拉取。

__public__: 公共资源目录。这些资源可以直接访问,而不需要 __public__ 前缀。举个例子:

代码语言:javascript
复制
__theme__/
  index.css  *# -> 访问链接 example.com/__theme__/index.css*
__public__/
  hello.html *# -> 访问链接 example.com/hello.html*

transpiler 的工作过程应该比较清晰了:

  • 扫描 __apps__ 下的子应用。开发者也可以在子应用目录下使用 mapp.json 显式定义子应用描述信息。扫描后的子应用信息将放在 microApps 变量下。
  • 扫描 __config__ 下的配置文件。解析出配置信息。
  • 扫描 __i18n__ 下的 .tr, 解析结果放在 i18n 变量下。
  • 扫描 __theme__ 目录。__theme__ 主题包也支持携带配置文件、语言包,所以这些信息也会合并到配置信息中。另外 CSS 文件、JavaScript 文件将被收集到 theme 变量中。

扫描完毕之后,transpiler 拿着配置信息进行模板转译,将 /data/source 下的静态资源转换被拷贝到 /data/public 目录下。

来看个实际的模板例子:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta name="description" content="[%= description %]" />
    <meta name="keywords" content="[%= keywords %]" />
    <link rel="icon" href="[%= assets.IMG_BAY_FAVICON || entryPath + '/favicon.png' %]" />
    <meta name="version" content="[%= version %]" />
    <meta name="update-time" content="[%= `${year}-${month}-${date}` %]" />
    <title>[%= title %]</title>
    <!--! [%- theme.stylesheets.map(i => `<link rel="stylesheet" href="${i + '?' + hash }" />`).join('\n') %] -->
    <!--! [%- theme.scripts.map(i => `<script async="true" src="${i + '?' + hash}"></script>`).join('\n') %] -->
    <!--! [% if (microApps.length) { %] -->
    <!--! [%- 
      `<script>
        // 微应用注入
        (window.__MAPPS__ = (window.__MAPPS__ || [])).push(${microApps.map(i => JSON.stringify(i)).join(', ')});
      </script>`
    %] -->
    <!--! [% } %]-->
    <!--! [%- `<script>
      // 静态资源注入
      (window.__MAPP_ASSETS__ = (window.__MAPP_ASSETS__ || [])).push(${JSON.stringify(assets)});

      // 全局共享的语言包
      window.__I18N_BUNDLES__ = ${JSON.stringify(i18n)};
    </script>` %] -->
    <script
      defer="defer"
      src="[%= cdnDomain ? '//' + cdnDomain : '' %][%= removeTrailingSlash(base) %]/__entry__/js/chunk-vendors.582ba02c.js?[%= hash %]"
    ></script>
    <script
      defer="defer"
      src="[%= cdnDomain ? '//' + cdnDomain : '' %][%= removeTrailingSlash(base) %]/__entry__/js/app.01bd68bb.js?[%= hash %]"
    ></script>
    <link
      href="[%= cdnDomain ? '//' + cdnDomain : '' %][%= removeTrailingSlash(base) %]/__entry__/css/app.d835cada.css?[%= hash %]"
      rel="stylesheet"
    />
  </head>

  <body>
    <noscript
      ><strong
        >We're sorry but [%= title %] doesn't work properly without JavaScript enabled. Please
        enable it to continue.
      </strong></noscript
    >
    <div id="app"></div>
  </body>
</html>

上面是基座的 index.html 模板。transpiler 基于 ejs 模板引擎,会解析替换文本文件中 [% 模板 %] 语法。

还有很多用法值得去挖掘。 比如:

  • 全局埋点脚本注入
  • 全局监控脚本注入

原理就是这么简单,只要子应用部署到 __apps__ 目录下,我们就可以监听到,收集到必要的信息后,对 source 目录下的静态文件进行转译,输出到 public 目录下,最终由 Nginx 负责将文件传递给浏览器。

部署和运维

那么子应用具体如何部署和运维呢?

子应用构建、生成和发布容器的过程这里就不展开说了,可以自行搜索 Docker 的相关教程,我们这里主要简单介绍一下在 K8S 平台如何部署和运维。

将基座和子应用聚合在一起,我们需要用到 PVC (PersistentVolumeClaim, 即持久化卷), 你可以认为 PVC 就是一个「网络硬盘」,而每个子应用、基座都是独立运行的「主机」(Pod 或 容器 , Kubernetes 中可部署的最小、最基本对象), 这个 PVC 可以被每个子应用共享访问,只要按照约定将子应用的静态文件拷贝到 PVC 对应位置就行了。如下图所示:

至于子应用和运行容器在 K8S 下如何组织,可以非常灵活,取决于需求和环境。笔者实践过以下几种方式:

  1. 全部部署在一个 Pod 下。子应用作为 Init Sidecar(初始化边车)。这种部署方式比较简单,缺点就是任意一个应用需要更新,整个 Pod 都要重启,包括运行容器。

示例图:

  1. 分离运行容器和子应用。为了避免子应用更新导致整个 Pod 重启(包括运行容器),我们可以将子应用单独拎出去,子应用更新只会重启所在的 Pod,从而避免运行容器停机。
  1. 每个子应用都是独立的 Pod。好处就是每个子应用可以真正做到独立部署、启动,坏处就是管理起来稍显麻烦。

开发者可以根据自己的运行环境选择不同的组织方式。

那么配置呢?

首先简单的配置可以通过环境变量来实现,因为在 K8S 中,配置环境变量相对简单很多:

对于稍微复杂的配置,可以使用配置映射(Config-Maps), 配置映射的每个键值对就相当于一个文件,我们可以挂载到容器的任何位置上:

定义配置映射:

挂载配置映射:

配置映射可以挂载到任意的路径或文件上,它还有一个更赞的能力是:我们可以直接修改配置映射,这些变动会同步到容器内,从而实现实时变更

小结。我们尽量复用了 K8S 本身的能力,这些能力足以实现较为复杂功能,避免重复造轮子。

更多

限于篇幅很多细节无法展开,这里点到为止:

  • 如何实现一键部署?因为使用的是容器化部署,可以将所有部署声明在 yaml 文件中维护, 新环境部署时直接导入就行。我们也开发过一个可视化生成 yaml 的简易应用
  • 自动化部署?实现自动化部署有很多手段,如果你的公司有 DevOps 平台(比如我们使用 Zadig) , 这些平台本身就提供了自动化部署的能力,你可以查看相关文档。另外在 Jenkins 中也有相关的插件来实现部署推送。再不济,可以使用 rancher 的 CLI 等等
  • 子应用如何共享依赖?可以使用 externals, 或者 Webpack 5 的 **Module Federation, 我们也探索过类似 jsDelivr 的方案, 详见这里
  • 接口服务。运行容器除了上文讲到的各种功能,还可以提供一些造福前端的接口服务,比如接口代理、polyfill 服务、vendor 依赖。
  • 安全配置。在运行容器中统一配置 CSP、跨域等安全配置

有哪些最佳实践?

  • 保持基座业务无关性。我们尽量保证基座不耦合业务,为了避免子应用的业务侵入到基座,我们严格管控基座仓库的开发权限,以及向下暴露接口的截面。
  • 保持子应用之间的独立性。基座除了 EventBus ,没有提供其他应用通信的手段。对我们来说,微前端只不过是多页应用的延续。 设计良好的应用,不应该耦合其他应用。就算是一些共享状态,也可以从后端读取。
  • 避免硬编码配置信息。因为运行容器有动态替换变量的能力,因此应该避免在代码中硬编码配置信息,比如域名信息、企业文案、服务器链接。而是预留模板, 在部署时通过运行容器来配置。
  • 按照业务聚合子应用。即按照业务边界来拆分子应用,而不是按照‘菜单’, 具体来说子应用应该对应后端的微服务,尽管很多时候做不到。

未来

我们整套方案并没有‘自造’复杂的技术,而是基于已有的工具整合起来的能力。这也是笔者一直坚持的观念,简单至上。

这个方案未来会如何迭代呢?

  • 可视化方式,简化部署的流程。毕竟不是所有开发者都熟悉 K8S 这套概念
  • 发布流程审核。生产环境部署审核。
  • 基座插件。支持扩展一些除子应用之外的场景,比如一些全局通用的业务 SDK、组件库。常规的子应用只会在路由匹配到时激活,而插件会在基座启动后加载并持久存在。
  • 支持子应用扩展服务端的能力。当前的子应用都是 CSR,后续运行容器可以支持子应用扩展服务端接口。
  • 灰度发布。
  • 支持 Vite

总结

本文大概介绍了我们落地和治理微前端应用的大概思路。这套体系中主要包含了三个主要部件:

  • 基座:集中式的微前端方案,基座是整个微前端的核心,负责管理子应用,并为子应用的开发提供必要的支撑
  • 子应用:负责具体业务实现,按照业务聚合和拆分。
  • 运行容器:为微前端应用架构提供了部署和治理方案

因为文章篇幅原因,这里面很多细节无法展开。感兴趣的可以移步我们公开的文档(暂未开源)。

扩展阅读

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-07-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景与痛点
  • 架构
  • 基座
  • 子应用接入
    • 配置
      • 挂载
        • 本地开发和调试
        • 部署和治理
          • 容器化
            • 运行容器
              • 如果实现子应用的自动发现?
                • 部署和运维
                  • 那么配置呢?
                    • 更多
                      • 有哪些最佳实践?
                        • 未来
                        • 总结
                        • 扩展阅读
                        相关产品与服务
                        容器服务
                        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档