微前端-一个巨型PC项目的拆分

1,847 阅读10分钟

内部分享文档,先在掘金打个草稿,分享下

一、前端发展简史

**2009年:**催生了web前端这一新专业。这一年出现了很多新变化,NodeJS的出世,JavaScript开始涉足后端。这一年,AngularJS的发布,给spa项目带来革命性指令时代,不仅大大提高了生产力,还带来了模块化,项目可维护性大幅提高,为云平台技术等大型软件在前端上提供了很好的技术支撑。但同时也要求更多专业人士做这些事。

**2011年:**React出现以来,带来了虚拟dom、模块标签语义化、jsx编程,fiber调度等技术和思想,模块化编程更简洁,更能代表技术发展的方向。React最大优点是标准化、体系化,编程体验应该说是当下最好的。

**2012年:**webpack诞生了,目的是解决打包JavaScript多模块编程的Code Splitting,通俗讲就是对模块的chunk处理。webpack使spa项目的性能得到了较好的管理。webpack在2014年加入热更新功能,开发体验也有了较大的提高。webpack为解决性能加入tree shaking等等,为解决打包慢加入的cache、TerserPlugin等等

**2014年:**结合angular和react等之长产生了Vue,新版本vue3同样借鉴了很多React16的函数式编程思想。Vue绝对称得上是吃百家饭长大的孩子,入门简单,发展的很好。

与此同时后端微服务也在慢慢发酵着

二、老旧前端工程化带来的问题

前端从后端服务分离,独立成为一个项目之后,随之而来的是前端工程化。

而绝大多数的前端项目都是单体项目,并且在2014年之前后端其实也是大规模的单体项目。

而单体项目存在的问题有着大约以下几点:

1、复杂性高

以一个百万行级别的单体应用为例,整个项目包含的模块非常多、模块的边界模糊、依赖关系不清晰、代码质量参差不齐、混乱地堆砌在一起…整个项目非常复杂。每次修改代码都心惊胆战,甚至添加一个简单的功能,或者修改一个Bug都会带来隐含的缺陷。

2、 技术债务

随着时间推移、需求变更和人员更迭,会逐渐形成应用程序的技术债务,并且越积越多。“不坏不修( Not broken, don not fix)",这在软件开发中非常常见,在单体应用中这种思想更甚。已使用的系统设计或代码难以被修改,因为应用程序中的其他模块可能会以意料之外的方式使用它。 

3、部署频宰低

随着代码的增多,构建和部署的时间也会增加。而在单体应用中:每次功能的变更或缺陷的修复都会导致需要重新部署整个应用。全量部署的方式耗时长、影响范围大、风险高,这使得单体应用项目上线部署的频率较低。而部署频率低又导致两次发布之间会有大量的功能变更和缺陷修复,出错概率比较高。 

4、可靠性差

某个应用Bug,例如死循环、OOM等,可能会导致整个应用的崩溃。 

5、扩展能力受限

单体应用只能作为一个整体进行扩展,无法根据业务模块的需要进行伸缩。

6、阻碍技术创新

单体应用生往使用统一的技术平台或方案解决所有的问题,团队中的每个成员都必须使用相同的开发语言和框架,要想引人新框架或新技术平台会非常困难。例如,一个使用 react构建的、有100 万行代码的单体应用,如果想要换用Vue,毫无疑问切换的成本是非常高的。

三、前端应用拆分方法

1、iFrame

  • 不是单页应用,不会影响外部的路由地址,无法记住当前访问的页面地址,会导致浏览器刷新页面 iframe url 状态丢失、后退前进按钮无法使用

  • 弹框类的功能无法应用到整个大应用中,只能在对应的窗口内展示。

  • 由于可能应用间不是在相同的域内,主应用的 cookie 要透传到根域名都不同的子应用中才能实现免登录效果。

  • 每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程,占用大量资源的同时也在极大地消耗资源。

  • iframe的特性导致搜索引擎无法获取到其中的内容,进而无法实现应用的 seo

2、后端模板方式

  • 通过传统的ejs等模板的方式页面各个部分或者不同页面不同模板的方式将页面组合起来
  • 古老的mvc项目架构模式,前后端不分离

3、不同项目拆分为npm包

  • 每次发布都需要发布到npm仓库
  • 无法实现多项目之间联调
  • 采用workSpcae(monorepo),所有项目还是在一个项目中,只是模块拆分和拆分目录效果类似

4、路由分发方式

  • 每个项目完全独立,数据互通交互困难
  • 公共模块无法复用

四、希望的架构模式和业务模式

  • 独立部署
  • 独立开发
  • 技术无关
  • 不影响用户体验 
  • 实时联调
  • 数据互通
  • 模块共享
  • 和技术无关

大致效果图

五、新时代的前端微服务方式

1、single-spa为代表的微前端框架

  1. 一个中心化应用控制路由分发
  2. 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  3. HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  4. 样式隔离,确保微应用之间样式互相不干扰。
  5. JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  6. 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

2、webpack5为代表的EMP微前端

  1. 无中心化项目每个项目都可以分享自己的模块
  2. 每个项目都可以是一个npm包,项目之间引用通过连接方式加载
  3. 要求技术栈全栈统一
  4. 内部代码引用就像import一个包一样

六、技术框架的选择和实践

1、为什么是qiankun.js

  • 从single-spa出现之后,qiankun是最早出现的微前端框架
  • qiankun基于single-spa但是没有single-spa的诸多缺陷
  • 中文文档
  • 开源社区微前端框架star数量最高
  • 众多文档完善
  • 个人已经有真实上线的业务

2、引入qiankun,接管主体框架的路由

通过官方的引导在项目全局引入qiankun,接管项目路由的分发,同时分发主应用数据,做好数据共享等

config.js

import vuex from '@/store'
const baseConfig = require('../../config/config')
const { publicPath } = baseConfig
import SvgIcon from '@/components/SvgIcon'

function getConfig() {
  const seting = {}
  const props = {
    mainPrefix: publicPath,
    vuex,
    components: {
      SvgIcon,
    },
  }
  switch (process.env.VUE_APP_NODE_ENV) {
    // 开发, 本地开发走vue代理
    case 'development': {
      break
    }
    // 测试环境
    case 'staging': {
      break
    }
    // 生产
    case 'production': {
      break
    }
  }

  const config = [
    {
      name: 'poster',
      entry: `//localhost:7001/poster/`,
      container: '#app-qiankun',
      activeRule: publicPath + '/#/posterToIn',
      props,
    },
  ]

  const fullscreenConfig = [
    {
      name: 'posterFullscreen',
      entry: `//localhost:7001/poster/`,
      container: '#fullscreen-qiankun',
      activeRule: publicPath + '/#/poster',
      props,
    },
  ]

  return {
    config,
    fullscreenConfig,
  }
}

export default getConfig

index.js

import { registerMicroApps, start, addGlobalUncaughtErrorHandler } from 'qiankun'
import getConfig from './config'
import { Message } from 'element-ui'

export default function startQiankun(
  isFullscreen = false,
  opt = {
    prefetch: true, // 是否开启预加载
    // 是否开启沙箱
    sandbox: {
      experimentalStyleIsolation: true, // 实验性的样式隔离特性
    },
    singular: true, // 是否为单实例场景
  },
) {
  const { fullscreenConfig, config } = getConfig()
  const loadCon = isFullscreen ? fullscreenConfig : config
  // const { fullscreenConfig, config } = getConfig()

  registerMicroApps(loadCon)
  start(opt)

  addGlobalUncaughtErrorHandler((event) => {
    const { msg } = event
    // 加载失败时提示
    if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
      Message.error('微应用加载失败,请检查应用是否可运行')
    }
  })
}

3、问题1,页面结构不单一,需要支持双容器

所以根据项目路由进行拆分判断,并且书写两个组件,分别支持两种结构的组件

以下是核心的路由判断部分,分为全屏组件和非全屏组件

import getConfig from './config'
import config from '/config/config'

// 默认返回false,表示未匹配到,true则匹配到情况
// matching,默认为匹配全屏参数,true, false匹配非全面路由
export function isMatchingRule(path = '', matching = true) {
  if (!path || path === '/') return false

  // 为了保证匹配的时候不会因为前缀一致导致页面判断错误
  if (path.substring(path.length - 1, path.length) !== '/') path = path + '/'

  const { fullscreenConfig, config: qiankunConfig } = getConfig()

  const matchConfig = matching ? fullscreenConfig : qiankunConfig
  // 非全屏的匹配前缀
  for (let i = 0; i < matchConfig.length; i++) {
    // 为了保证匹配的时候不会因为前缀一致导致页面判断错误
    const activeRule =
      matchConfig[i].activeRule.substring(path.length - 1, path.length) === '/'
        ? matchConfig[i].activeRule
        : matchConfig[i].activeRule + '/'
    //const rex = new RegExp('^' + activeRule + '$')

    // 哈希路由特殊处理
    if (path.substring(0, 2) === '/#') {
      path = config.publicPath + path
    }

    // console.log(
    //   'qiankun匹配路由',
    //   activeRule,
    //   path,
    //   activeRule === path.substring(0, activeRule.length),
    // )
    if (activeRule === path.substring(0, activeRule.length)) return true
  }

  return false
}

fullscreenQiankun.vue全屏容器

<template>
  <div id="fullscreen-qiankun"></div>
</template>

<script>
import startQiankun from '@/qiankun'
export default {
  mounted() {
    startQiankun(
      true, // 全屏存在
    )
  },
}
</script>

<style scoped lang="scss">
#fullscreen-qiankun {
  //position: absolute;
  //left: 0;
  //top: 0;
  width: 100%;
  height: 100%;
}
</style>

inline-content.vue非全屏容器

<template>
  <div id="fullscreen-qiankun"></div>
</template>

<script>
import startQiankun from '@/qiankun'
export default {
  mounted() {
    startQiankun(
      true, // 全屏存在
    )
  },
}
</script>

<style scoped lang="scss">
#fullscreen-qiankun {
  //position: absolute;
  //left: 0;
  //top: 0;
  width: 100%;
  height: 100%;
}
</style>

4、App.vue改造

全屏组件和实际主项目页面要监听加载

<fullscreen-qiankun v-if="isMatchingRule"></fullscreen-qiankun>
    <router-view v-show="!isMatchingRule" />
watch: {
    $route: {
      immediate: true,
      deep: true,
      handler(route) {
        this.isMatchingRule = isMatchingRule(route.fullPath)
      },
    },
  },

左侧菜单结构固定格式的容器加载

<page-content v-show="!isMatchingRule"></page-content>

      <qiankun-content v-show="isMatchingRule"></qiankun-content>
watch: {
    $route: {
      immediate: true,
      deep: true,
      handler(route) {
        if (route.fullPath === '/') {
          // 是否重定向到首页
          this.$router.push({ path: '/homepage' })
        } else {
          this.isMatchingRule = isMatchingRule(route.fullPath, false)
          this.fullscreen = isMatchingRule(route.fullPath, true)
        }
      },
    },
  },

5、子项目改造,main.js改造

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

import 'normalize.css/normalize.css' // 初始化默认css
import Vue from 'vue'
import App from './App.vue'

import { routerConfig } from './router'
import VueRouter from 'vue-router'

import store from './store'
import '@/components/index'
import '@/styles/index.scss' // 全局样式定义和样式开发规范
const config = require('/config/config')

import { setVue2Vom } from './qiankunShared/components'

Vue.config.productionTip = false

Vue.use(VueRouter)

let router = null
let instance = null

function render(props = {}) {
  const { container, mainPrefix, vuex, components } = props
  setVue2Vom(components)

  store.commit('user/SET_NAME', vuex.state.user.name)
  store.commit('user/SET_TOKEN', vuex.state.user.token)

  // 路由处理
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? mainPrefix : config.publicPath,
    mode: 'hash',
    routes: routerConfig,
  })

  // 实例挂载
  instance = new Vue({
    router,
    store,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app')
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

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 = null
}

6、未完全展示部分

以上是主要核心的实现部分

还未展示部分

1、子项目模板的开发,便于新项目启动和快速接入

2、组件共享机制未编写(实验通过)

3、子项目之间共享组件

4、主子项目和子子项目直接的数据通信

七、总结

1、微前端的缺点

  • 复杂度从代码转向基础设施

  • 整个应用的稳定性和安全性变得更加不可控

  • 具备一定的学习和了解成本

  • 需要建立全面的微前端周边设施,才能充分发挥其架构的优势

  • 调试工具

  • 监控系统

  • 上层 Web 框架

  • 部署平台

2、微前端的优点

  • 适用于大规模 Web 应用的开发
  • 更快的开发速度
  • 支持迭代可开发和增强升级
  • 拆解后的部分降低了开发者的理解成本
  • 同时具备 UX 和 DX 的开发模式

3、何时使用微前端

  • 大规模企业级 Web 应用开发

  • 跨团队及企业级应用协作开发

  • 长期收益高于短期收益

  • 不同技术选型的项目

  • 内聚的单个产品中部分需要独立发布、灰度等能力

  • 微前端的目标并非用于取代 Iframe

  • 应用的来源必须可信

  • 用户体验要求更高

4、微前端并非万能

采用微前端后复杂度并未凭空消失,而是由代码转向了基础设施,对架构设计带来了更大的挑战,并且在新的架构下需要设计并提供更多的周边工具和生态来助力这一新的研发模式。

参考资料:

1、后端微服务发展史:www.jianshu.com/p/d3144d64b…

2、微前端产生的历史背景和作用:zhuanlan.zhihu.com/p/344145423

3、5分钟搞懂Monorepo:www.jianshu.com/p/c10d0b8c5…

4、字节跳动是如何落地微前端的:juejin.cn/post/701691…