前言
微前端系列分为 上/下 两篇,本文为 上篇 主要还是了解微前端的由来、概念、作用等,以及基于已有的微前端框架进行实践,并了解微前端的核心功能所在,而在下篇 2022 你还不会微前端吗 (下) — 揭秘微前端核心原理 中主要就是通过自定义实现一个微前端框架来加深理解。
微前端是什么?
微前端 是一种类似于 微服务 的概念,因此要想更好的了解微前端,就必须先了解一下微服务。
微服务
微服务架构 是将一个庞大的业务系统按照业务模块拆分成 若干个独立的子系统,每个子系统都是一个独立的应用,它是一种将应用构建成一系列按 业务领域 划分模块的、小的自治服务的软件架构方式,倡导将 复杂的单体应用 拆分成 若干个功能单一、松偶合的服务,目的是降低开发难度、增强扩展性、便于敏捷开发,及持续集成与交付活动。
与微服务相对的另一个概念是传统的 单体式应用程序( Monolithic application ),单体式应用内部包含了 所有需要的服务,且各个服务功能模块具有 强耦合性(相互依赖),导致难以进行拆分和扩容。
简单来说,单体式应用程序 其实就是一台服务器处理了需要所有的功能,微服务 就是将功能按照业务模块划分成了不同的独立服务,各个微服务间通过 HTTP 协议进行通信,通过注册中心观测微服务状态。
微服务 概念主要存在于后端开发,但这个概念是不是和你听说过的 微前端 很像了。
微前端
随着大前端的快速发展 和 SPA
的大规模应用,也带来了新的问题,而这些问题都催化出了 微前端 的概念:
- 项目功能不断增多、体积不断增大(
巨石应用
),导致打包时间成正比例增长,是否能保证更好的项目扩展
- 前端技术更新太快,一个项目历经一两年也许就需要进行项目升级,甚至是切换技术栈,但仍需要老项目的代码,是否能进行
新老版本的兼容
- 团队技术栈不一,又需要保证同一项目的开发,是否能保证不同团队的
独立开发
- SPA 项目的任何变动都需执行完整的打包、部署,是否能保证不同内容
独立部署
微前端 是一种类似于 微服务 的架构,是一种由独立交付的 多个前端应用 组成整体的架构风格,将前端应用分解成一些更小、更简单的能够 独立开发、测试、部署 的应用,而对外表现仍是 单个内聚的产品。
微前端框架
微前端框架的核心
一个微前端框架至少要保证如下的核心功能:
- 技术栈无关
- 主框架不限制接入子应用的技术栈,微应用具备完全自主权
- 独立开发、独立部署
- 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级
- 在面对各种复杂场景时,通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时
- 每个微应用之间状态隔离,运行时状态不共享
single-spa
single-spa
是一个将多个单页面应用聚合为一个整体应用的 JavaScript
微前端框架。
核心原理
在 基座 (主) 应用 中注册所有 App
的路由,single-spa
保存各子应用的路由映射关系,充当微前端控制器 Controler,当对应的 URL
变换时,除了匹配 基座应用 本身的路由外,还会匹配 子应用 路由并加载渲染子应用。
子应用会经过如下过程:
- 下载 (
loaded
) - 初始化 (
initialized
) - 挂载 (
mounted
) - 卸载 (
unmounted
)
single-spa
还会通过 生命周期 为这些过程提供对应的 钩子函数。
qiankun
qiankun
是一个基于 single-spa
的 微前端 实现库,目的是提供更简单、无痛的构建一个生产可用微前端架构系统。
包括在 single-spa
文档中也有推荐使用 qiankun
:
single-spa 实践
创建子应用
Vue3 子应用
为了快速的创建应用,这里通过 vue create vue3-micro-app
来快速创建技术栈为 Vue3
的 子应用。
以下在子应用中的处理方式可用
single-spa-vue
来简化
页面效果如下
子应用入口文件
为了子应用既可以独立运行,也可以在基座应用中运行,需要在子应用入口文件进行一些修改,具体如下:
- 将原本的初始化内容封装在自定义的
render
函数中,目的是可以在不同的环境执行初始化操作
- 若当前在基座应用中进行渲染,则其页面内容对应的挂载容器需要指定为基座容器中对应的
DOM
节点 - 当
window.singleVue3
不存在时意味着是子应用独立运行,此时直接按照原本的初始化方式进行即可,即直接调用render()
函数
- 子应用必须导出
bootstrap、mount、unmount
等生命周期函数,且其返回值类型要为fullfilled
状态的Promise
,否则后续操作不会执行 - 定义
instance
变量存储实例对象,方便在当前子应用在基座应用中被切换时可以执行真正的卸载子应用
// main.ts import { createApp } from 'vue' import type { App as AppType } from 'vue' import App from './App.vue' import router from './router' let instance: AppType function render(container?: string) { instance = createApp(App) instance.use(router).mount(container || '#micro-vue-app') } // 当 window.singleVue3 不存在时,意味着是子应用单独运行 if (!window.singleVue3) { render(); } // 子应用必须导出 以下生命周期 bootstrap、mount、unmount export const bootstrap = () => { return Promise.resolve() }; export const mount = (props: any) => { render(props.container); return Promise.resolve() }; export const unmount = () => { instance.unmount(); return Promise.resolve() }; 复制代码
为什么要将
x.mount('#app')
换成x.mount('#micro-vue-app')
?
如果你明白 子应用 在 基座应用 中的渲染方式就不难理解了,因为当前这个子应用的挂载容器的 id="app"
而基座应用中的默认挂载容器也是 id="app"
,这显然会导致冲突,初始化渲染时会渲染基座应用本身,但是当你切换到 vue3
的子应用时,就会发现当前子应用的内容整个覆盖了基座应用的内容,因为此时子应用在进行挂挂载的时候,会把已经渲染 基座应用 的容器再一次作为 子应用 的容器进行渲染,于是内容就会被完全替换成子应用的内容。
基座应用被子应用替换效果如下:
路由配置
路由模式为 hash
模式,默认路由配置:
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' import HomeView from '../views/HomeView.vue' const routes: Array<RouteRecordRaw> = [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue') } ] const router = createRouter({ history: createWebHashHistory(), routes }) export default router 复制代码
打包配置
在 vue.config.js
下必须将打包后的输出格式指定为 umd
格式:
module.exports = { configureWebpack: { output: { library: 'singleVue3', libraryTarget: 'umd', globalObject: 'window', }, devServer: { port: 5000, }, }, } 复制代码
React 子应用
类似的,这里通过 npx create-react-app react-micro-app
来快速创建技术栈为 React
的 子应用。
以下在子应用中的处理方式可用
single-spa-react
来简化
页面效果如下
子应用入口文件
此部分核心内容和上述的 vue3
子应用一致,不在额外说明,入口文件代码如下:
// index.js import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' import App from './App' let root = null function render(props = {}) { const container = document.getElementById( props.container ? props.container.slice(1) : 'root', ) if(!container) return root = ReactDOM.createRoot(container) root.render( <React.StrictMode> <App {...props} /> </React.StrictMode>, ) } // 当 window.singleReact 不存在时,意味着是子应用单独运行 if (!window.singleReact) { render() } // 子应用必须导出 以下生命周期 bootstrap、mount、unmount export const bootstrap = () => { return Promise.resolve() } export const mount = (props) => { render(props) return Promise.resolve() } export const unmount = () => { root.unmount() return Promise.resolve()