微前端

字数 6254 阅读 352 喜欢 1

作为前端开发人员,大家一直都在开发单体应用,虽然有将代码划分为组件,功能拆分为不同模块,通用方法抽离等等,然后使用require或者import引入使用,或者通过npm将package.json中定义好的npm包或者已安装好的子git仓库添加到项目中,最终构建成一个完整的整体。

为什么目前的应用是单体?

单体应用的概念是指,当前应用无法给其他应用提供服务帮助(或者提供相对困难),应用当前只满足自己应用的使用

除了目前实现了微前端的应用之外,所有的前端应用从本质上将都是单一的应用。但是当团队中不同的人员进行不同模块的开发,或者使用的vue或者npm包(甚至使用的框架都不一样的时候),在进行协作开发以及部署的同时,始终会出现代码冲突的问题。因为它们没有完全分离,很可能它们维护着相同的仓库并具有相同的构建系统。单体应用的退出被标志为微服务的出现。

什么是微服务?

对于微服务,一般而言最简单的解释是,它是一种开发技术,允许开发人员为平台的不同部分进行独立部署,而不会损害其他部分。独立部署的能力允许他们构建孤立或松散耦合的服务。为了使这个体系结构更稳定,有一些规则要遵循,可以总结如下:每个服务应该只有一个任务,它应该很小。所以负责这项服务的团队应该很小。

我找了一个对于微服务比较直观的图片
https://static.jinhaidi.cn/images/20200421162132.png?jhd_blog_image

从图上我们可以看到 每个微服务都是一个独立的应用,但是除了UI,UI仍然是一体的!当业务需求进行快速扩张或者跨职能小团队进行敏捷开发的时候,前端团队将开始重复实现类似的界面或者ui层面的渲染,这是一种资源浪费,也是目前架构设计的瓶颈所在。针对目前面临,以及即将面临的诸多问题,几年前出现了微前端的想法并且快速普及。

微前端能解决我们什么问题?

什么是微前端

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端架构具备以下几个核心价值:

  • 技术栈无关
    主框架不限制接入应用的技术栈,微应用具备完全自主权

  • 独立开发、独立部署
    微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

  • 增量升级

    在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

  • 独立运行时
    每个微应用之间状态隔离,运行时状态不共享

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用( Frontend Monolith )后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

需要解决哪些问题

  1. 新版项目与旧版项目能够同时存在,解决了相同业务功能重复编写的问题。
  2. 子项目更好维护(只需要关注当前项目需要关注的业务就可以,短小精悍)。
  3. 更快速的渲染速度以及更好的用户体验
  4. 更灵活的产品设计
  5. 新的技术尝试

微前端的实现方案

方式 成本 维护成本 可行性 统一框架要求 实现难度 潜在风险 其他
路由分发 方案普通、并且需要特殊配置 工程无法复用、耦合性太强
iframe ★★ 跨域配置、安全隐患 复用性差、耦合性强、移动端适配困难、容易产生性能方面
应用微服务话 ★★★★ 针对不同微应用定制及hook 不同微应用可有自己单独配置、降低自身耦合性、提升复用的可能性
微件化 ★★★★ 工程功能进行组件化开发,维护成本高、开发效率低 复用性强、但不适合进行工程开发
Web Components ★★★ 浏览器兼容问题、新技术 复用性强,主流框架支持 Web Components

根据上面的表格
基本要求就是寻找适合当前开发形式,并且对于工程侵入比较小的解决方案,并且需要满足开发成本小、复用性强、灵活等多方面要求

single-spa

Single-spa 是一个将多个单页面应用聚合为一个整体应用的 javascript 微前端框架。 使用 single-spa 进行前端架构设计可以带来很多好处,例如:

在同一页面上使用多个前端框架 而不用刷新页面 (React, AngularJS, Angular, Ember, 你正在使用的框架)
独立部署每一个单页面应用
新功能使用新框架,旧的单页应用不用重写可以共存
改善初始加载时间,迟加载代码

存在问题:

  1. 对于基础通信方面需要自己实现(可用customerEvent、发布于订阅模式自己实现,没有范式要求多应用存在会出现性能问题)
  2. 对于公用组件 公用方法没有较好的解决方案
  3. 定制化要求成本高,需个人进行定制化开发
  4. 没有进行样式,变量等进行隔离操作,会照成全局污染

qiankun

基于single-spa实现库

  • 🥄 简单

    由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。

  • 🍡 解耦/技术栈无关

    微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。

特性

  • 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
  • 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  • 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  • 🛡​ 样式隔离,确保微应用之间样式互相不干扰。
  • 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  • ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
  • 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。

数云架构设计图

https://static.jinhaidi.cn/images/20200426143530.png?jhd_blog_image

配置设置

主应用设置

// main.js

import { registerMicroApps, start, initGlobalState, setDefaultMountApp } from 'qiankun'

function showWhenAnyOf(routes: any[]) {
  return (location: any) => {
    return routes.some(route => location.pathname === `${process.env.BASE_URL}${route}`)
  }
}

function showExcept(routes: any[]) {
  return (location: any) => {
    return routes.every(route => location.pathname !== `${process.env.BASE_URL}${route}`)
  }
}

const authRoute = ['', 'login', 'contact']
const v1Route = ['dashboard', 'about']
// 注册子应用
registerMicroApps([
  {
    name: 'app1',
    entry: '/app1/',
    container: '#app-main-app',
    activeRule: showWhenAnyOf(v1Route),
    props: {}
  },
  {
    name: 'app2',
    entry: '/app2/',
    container: '#app-main-app',
    activeRule: showExcept([...v1Route, ...authRoute])
  }
])

// 设置默认子应用
setDefaultMountApp('/app1/')

// 设置全局通用state
export const { onGlobalStateChange, setGlobalState } = initGlobalState({
  user: null,
  baseUrl: process.env.BASE_URL ? process.env.BASE_URL.substring(0, process.env.BASE_URL.length - 1) : ''
})

onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev))


start({})

子应用配置

// main.js
let baseUrl = ''
let router = null
let instance: any = null
// tslint:disable-next-line:variable-name
const __qiankun__ = (window as any).__POWERED_BY_QIANKUN__

function render(props = {} as any) {
  const { container } = props
  routes.map(item => item.path = `${baseUrl}${item.path}`)
  router = new VueRouter({
    base: (window as any).__POWERED_BY_QIANKUN__ ? '/app1' : '/',
    mode: 'history',
    routes
  })

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

export async function bootstrap(props: any) {
  console.log(props)
}
export async function mount(props: any) {
  console.log(props)
  // tslint:disable-next-line: no-unused-expression
  props.onGlobalStateChange &&
    props.onGlobalStateChange(
      (value: any, prev: any) => {
        store.commit('user/SET_USER', { key: 'user', value: prev.user })
        baseUrl = prev.baseUrl
      },
      true
    )
  render(props)
}
export async function unmount() {
  instance.$destroy()
  instance = null
  router = null
}
// tslint:disable-next-line:no-unused-expression
__qiankun__ || mount({})
// export const Router = router
export default instance
// vue.config.js
module.exports = {
    publicPath: process.env.NODE_ENV === 'production' ? '/app1' : '/',
    devServer: {
        headers: {
          'Access-Control-Allow-Origin': '*',
        }
    },
    configureWebpack: () => {
        const config = {}
        Object.assign(config, {
          output: {
            //把子应用打包成 umd 库格式
            library: '[name]',
            filename: '[name].js',
            libraryTarget: 'umd',
            globalObject: 'this',
          }
        })
        return config
    }
}