学习视频🔊
基础: 黑马前端基于qiankun搭建微前端项目实战教程_哔哩哔哩_bilibili
路由、部署配置注意:qiankun+vite微前端上线注意事项,base公共路径设置_哔哩哔哩_bilibili
微前端
什么是微前端?
微前端是将前端应用分解成一系列更小、更易管理的独立部分的架构方案。类似于微服务,每个微应用可以由不同团队独立开发、测试、部署。
微前端的好处?
-
技术栈无关
主应用和子应用可以使用不同的技术栈
允许渐进式技术栈升级
降低全局技术升级的风险
-
独立开发部署
各团队可以独立开发、测试、部署
有哪些微前端方案?
下面介绍有哪些主流的微前端方案。
1、qiankun(蚂蚁金服,后续也采用这种)
优点:
-
基于 single-spa 封装
-
完善的沙箱机制
-
开箱即用的 API
-
中文社区活跃
适用场景:
-
大型中台系统
-
需要多团队协作的项目
2、single-spa
优点:
-
最早的微前端框架
-
灵活性高
-
社区成熟
缺点:
-
配置较复杂
-
需要自己实现样式隔离
3、Module Federation(来自Webpack 5,中文:模块联邦,可以用来做远程组件)
优点:
-
Webpack 原生支持
-
真正的运行时模块共享
-
构建时优化
适用场景:
-
新项目
-
需要精细化控制模块共享
4、micro-app(京东)
优点:
-
使用简单
-
基于 Web Components
-
性能好
适用场景:
-
对性能要求高的项目
-
喜欢简单配置的团队
5、Iframe
优点:
-
具有天然的隔离属性,js沙箱、样式隔离等都很好。
缺点:
-
UI不同步,比如在iframe中添加蒙层弹框,只会在iframe中显示,不是全屏的。
-
慢,每次进入会重新加载,多个iframe时浏览器容易卡死。
基础改造
🌈🌈🌈
基座改造🚩
一、Antd Pro基座改造(umi系)🤠
接下来对中台(antd Pro 创建的项目,作为基座)进行改造。
1、安装@umijs/plugin-qiankun
【umimax已内置】
pnpm i @umijs/plugin-qiankun
2、注册子应用
config/config.ts配置:
在config的qiankun.master.apps数组中 注册子应用
export default defineConfig({qiankun: {master: {apps: [{name: 'sub-umi',//子应用的名称entry: '//localhost:5175',//子应用的入口地址activeRule: '/qiankun/umi',//子应用的激活规则,指路由sandbox: {strictStyleIsolation: true,//严格样式隔离},},],},},//其他配置
})
3、配置访问子应用的路由
config/router.tsx :在基座的路由中添加能访问子应用的路由。
方式1:用microApp属性指定要渲染的子应用的name。(本次改造采用这种)
{path: '/qiankun',name: 'qiankun',routes: [{path: '/qiankun/umi',name: 'sub-umi',microApp: 'sub-umi', //和注册时的name一致microAppProps: {// 子应用自动设置loading// autoSetLoading: true, //可以用autoSetLoading,需要子应用引入antdloader: (loading: boolean) => <Spin spinning={loading} />,},},],},
方式2:使用component属性指定组件,在组件中使用qiankun提供的 MicoApp组件
// 1、路由{path: '/app1',name: 'sub-app',element:<SubApp/>}// 2、 SubApp组件,用MicroApp组件占位,需要指定name(和注册时同名)
import React from 'react';
import { MicroApp } from '@umijs/max';
type Props = {};const MicroApp1 = (props: Props) => {return <MicroApp name="sub-app" />;
};export default MicroApp1;
二、非umi系基座改造
1、安装qiankun
pnpm i qiankun // 或者 yarn add qiankun
2、修改入口文件
在src/index.tsx中增加如下代码:从qiankun中引入注册和启动的函数,注册子应用并调用start启动。
import { start, registerMicroApps } from 'qiankun';// 1. 要加载的子应用列表
const apps = [{name: "sub-react", // 子应用的名称entry: '//localhost:8080', // 默认会加载这个路径下的html,解析里面的jsactiveRule: "/sub-react", // 匹配的路由container: "#sub-app" // 加载的容器},
]// 2. 注册子应用
registerMicroApps(apps, { //下面的配置对象可以不写beforeLoad: [async app => console.log('before load', app.name)],beforeMount: [async app => console.log('before mount', app.name)],afterMount: [async app => console.log('after mount', app.name)],
})start() // 3. 启动微服务
/*
// 配置qiankun启动参数
start({// prefetch: true, // 预加载 默认是true,即在主应用加载的时候,加载子应用sandbox: {//沙箱// experimentalStyleIsolation: true, // 实验性样式隔离,好像没用哦strictStyleIsolation: true, // 严格样式隔离},
})
*/
3、注册子应用路由,提供容器dom
router注册一下子应用的路由,element设置为null,在跳转到子应用的路由时,展示id为subApp的div。
//router/index.jsx
const router = [{path: "/",element: <Home />,},{path: "/app1/*",element: null,},
]//App.jsx
function App() {const element = useRoutes(router)return (<div className="main-layout"><nav><Link to="/">首页</Link><Link to="/app1">子应用1</Link></nav><div className="test-aa">123</div><div className="main-content"><Suspense fallback={<div>Loading...</div>}>{element}{/* 需要子应用的容器 */}<div id="subApp"></div></Suspense></div></div>)
}
一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑。 所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
-
registerMicroApps(apps, lifeCycles?)
注册所有子应用,qiankun会根据activeRule去匹配对应的子应用并加载
-
start(options?)
启动 qiankun,可以进行预加载和沙箱设置,更多options : API 说明 - qiankun
至此基座基本就改造完成,接下来改造子应用。
子应用改造🤩
子应用改造主要需要注意:
-
用umd格式打包,当然,像umi,vite这些插件已经设置了。
一、umi子应用改造
中台项目的子应用是采用umi进行构建。
-
安装插件
在子应用目录安装@umijs/plugins插件,才能在umirc中用qiankun字段。
pnpm i @umijs/plugins
-
使用插件
在 .umirc.ts 中使用上面的插件,这样就在基座中通过子应用的地址来访问这个子应用了。
export default defineConfig({base: '/', // 用qiankun插件后默认base为包名,所以这里重置一下qiankun: { //告诉umi这个项目需要用到qiankunslave: {},},plugins: ['@umijs/plugins/dist/qiankun', '@umijs/plugins/dist/model', '@umijs/plugins/dist/mf'], //plugins使用@umijs/plugins插件,功能分别是支持qiankun、允许useModel、模块联邦mf})
-
生命周期
如果需要在生命周期中做一些事情,可以在入口文件app.tsx中导出qiankun对象,在对象中的方法写代码,qiankun会执行这些生命周期函数。
export const qiankun = {async mount(props: any) {console.log(props)},async bootstrap() {console.log('umi app bootstraped');},async afterMount(props: any) {console.log('umi app afterMount', props);},
};
二、vue3+vite改造
创建子应用
创建子应用,选择vue3+vite
npm create vite@latest
改造子应用
-
安装
vite-plugin-qiankun
包,因为qiankun和vite有些问题,需要这个包解决。
pnpm i vite-plugin-qiankun
-
修改vite.config.js,使用上面的插件
import qiankun from 'vite-plugin-qiankun';defineConfig({base: '/sub-vue', // 和基座中配置的activeRule一致server: {port: 3002,cors: true,origin: 'http://localhost:3002'},plugins: [vue(),qiankun('sub-vue', { // 配置qiankun插件useDevMode: true})]
})
-
修改main.ts
我们需要提供三个必须的生命周期函数,即:bootstrap(只在第一次进入的时候执行)、mount(挂载)、onmount(卸载)
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';let app: any;
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {createApp(App).mount('#app');
} else {renderWithQiankun({// 子应用挂载mount(props) {app = createApp(App);app.mount(props.container.querySelector('#app'));},// 只有子应用第一次加载会触发bootstrap() {console.log('vue app bootstrap');},// 更新update() {console.log('vue app update');},// 卸载unmount() {console.log('vue app unmount');app?.unmount();}});
}
三、create-react-app改造
1、改造入口文件
代码如下
-
导出三个必须的生命周期函数,供qiankun使用。
-
根据window.__POWERED_BY_QIANKUN__来决定render逻辑
let root: Root// 将render方法用函数包裹,供后续主应用与独立运行调用
function render(props: any) {const { container } = propsconst dom = container ? container.querySelector('#root') : document.getElementById('root')root = createRoot(dom)root.render(// 可以根据需要指定basename//<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? "/sub-react" : ""}><BrowserRouter><App/></BrowserRouter>)
}// 判断是否在qiankun环境下,非qiankun环境下独立运行
if (!(window as any).__POWERED_BY_QIANKUN__) {render({});
}// 各个生命周期
// bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
export async function bootstrap() {console.log('react app bootstraped');
}// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
export async function mount(props: any) {render(props);
}// 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
export async function unmount(props: any) {root.unmount();
}
2、新增public-path.js
动态设置 webpack publicPath,防止资源加载出错
if (window.__POWERED_BY_QIANKUN__) {// 动态设置 webpack publicPath,防止资源加载出错// eslint-disable-next-line no-undefwebpack_public_path = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
3、修改webpack配置文件,用umd格式打包
这里用craco修改webpack的配置,所以应该先下载craco。在根目录下新增craco.config.js文件并新增如下配置
const { name } = require("./package");module.exports = {webpack: (webpackConfig) => {webpackConfig.output = {...webpackConfig.output,library: ${packageName}-[name],libraryTarget: "umd", //主要是这个配置,用umd打包这个项目chunkLoadingGlobal: webpackJsonp_${packageName},filename: "static/js/[name].umd.js",chunkFilename: "static/js/[name].umd.chunk.js",}return webpackConfig}
};
什么是umd?
umd格式_umd 库格式-CSDN博客
在基座能访问子应用,即说明配置成功。
说明
一、样式隔离
使用
子应用之间的样式隔离qiankun已经实现了,但是基座和子应用之间的样式隔离没有实现。
我们可以在基座注册子应用时 设置 strictStyleIsolation为true ,这样设置主要是对直接设置class时进行隔离(className='test'),在项目中我们一般是 css module 和strictStyleIsolation 一起使用,子应用能加自己的前缀是更好的。
export default defineConfig({qiankun: {master: {apps: [{name: 'sub-umi',entry: '//localhost:5175',activeRule: '/qiankun/umi',sandbox: {strictStyleIsolation: true,//严格样式隔离},},],},},//其他配置
})
strictStyleIsolation原理⛔️
【基于shadow dom来做的样式隔离】
shadowDOM 的MDN地址如下:
使用影子 DOM - Web API | MDN
strictStyleIsolation
的原理是基于 Web Components 中的 Shadow DOM 技术。让我详细解释一下:
-
基本实现原理:
function createShadowContainer(container, appName) {// 创建 Shadow DOMconst shadow = container.attachShadow({ mode: 'open' });// 子应用的所有内容都会被放入这个 Shadow DOM 中return shadow;
}
实际效果:
<!-- 普通 DOM 结构 -->
<div id="main"><div id="app1">#shadow-root (open)<!-- 子应用的所有内容都在 Shadow DOM 中 --><style>.title { color: red; }</style><div class="title">我是app1的标题</div></div>
</div>
-
Shadow DOM 的特性:
-
独立的 DOM 树
-
样式完全隔离
-
JavaScript 访问限制
-
事件局部化
-
样式隔离效果:
/* Shadow DOM 内部的样式 */
.title { color: red; }/* 外部的样式无法影响到 Shadow DOM 内部 */
#main .title { color: blue; } /* 这个样式不会影响 Shadow DOM 内的 .title */
主要优点:
-
完全的样式隔离
-
不需要额外的样式转换
-
原生的隔离方案
主要缺点:
-
一些第三方库可能无法正常工作
-
弹窗类组件可能会被限制在 Shadow DOM 内
-
浏览器兼容性问题
二、js沙箱
-
在基座中修改window会共享到各个子应用。
-
在子应用A修改window不会影响到子应用B。
qiankun中js沙箱的原理
qiankun中的js沙箱是对window进行隔离,主要解决全局变量冲突和全局状态相互影响的问题。
qiankun提供了三种沙箱模式:
1、在不支持proxy的浏览器,提供【快照沙箱】,在进入子应用之前
2、在支持proxy的浏览器,用proxy代理window,子应用修改代理后的window。单例就采用legacySandbox沙箱,多例就采用proxySandbox沙箱。
类似代码:
const proxy = new Proxy(window)
(function(window){//子应用的代码
})(proxy)
倔金沙箱:
jhttps://juejin.cn/post/6920110573418086413#heading-12
三、剔除重复依赖
如果基座和子应用使用了项目的库,可以考虑子应用使用基座的包,从而减少重复加载
有两种方式:
-
externals,基座用cdn引入包,子应用相同的cdn设置为ignore。(更推荐用externals)
-
模块联邦,基座将重复包打包至remote.js,子应用不打包重复的包,而是在运行时请求基座的remote.js
externals
流程:
-
基座:将所有公共依赖配置
webpack
的externals
,并且在index.html
使用外链引入这些公共依赖 -
子应用:和主应用一样配置
webpack
的externals
,并且在index.html
使用外链引入这些公共依赖,注意,还需要给子应用的公共依赖的加上ignore
属性(这是自定义的属性,非标准属性),qiankun在解析时如果发现igonre
属性就会自动忽略
以lodahs为例:
基座:
修改config/config.ts文件,在externals中添加lodash,之后在headScripts数组中添加lodash的cdn地址。
// 修改config/config.js
export default defineConfig({/*** @name <head> 中额外的 script* @description 配置 <head> 中额外的 script*/headScripts: [// 解决首次加载时白屏的问题//{ src: '/scripts/loading.js', async: true },//lodash 的cdn{ src: 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js', async: false },],externals: {lodash: '_',//externals 指定lodash和他的全局变量名},
})
umi子应用:
子应用同样需要在自己的配置文件中添加cdn的lodash,并且需要添加ignore忽略lodash。
<!-- 注意:这里的公共依赖的版本,基座和子应用需要一致 -->
export default defineConfig({// 剔除重复包externals: {lodash: '_',},headScripts: [{src: 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js',async: false,ignore: true, // 子应用需要添加ignore,忽略lodash,使用基座挂载window上的lodash,这样只需要请求一次},],
})
四、公共组件
我们后续会用pnpm的workspace来做monorepo,这样在单仓下基座和子应用就能共享组件了,
但是这样还有一个问题,当公共组件变化,子应用就需要重新打包部署才能得到公共组件的变化,所以可以采用模块联邦的方式,让子应用使用基座的远程组件,这样就只需要对基座进行打包部署。
接下来先介绍monorepo改造基座,并举例在子应用中如何使用workspce的公共组件,最后介绍将公共组件转为远程组件使用。
1、monorepo改造
-
在基座的根目录新建
pnpm-workspace.yaml
文件,文件内容:
意思是:
-
会将这三个文件夹下的目录添加到workspace工作空间中,他们可以相互通过workspce访问到。
-
其中app文件夹存放子应用的项目代码,src/expoese/components文件下存放各种公共组件的代码。
-
在umi中要导出远程组件,需要将组件写到src/exposes文件夹下,umi自动处理exposes文件夹下的组件。
packages:- 'apps/*'- 'src/exposes/components/*'- 'src/exposes/*'
-
新建yaml中涉及的文件夹,将子应用放入apps文件夹中。
至此简单的monorepo改造完毕
2、添加公共组件
下面演示PureButton
这个公共组件的创建和使用。
-
前置步骤
在src/exposes/components (看上面,这是一个workspace目录) 目录下执行npm init -y
,我们会将components目录作为组件库的目录,后续的公共组件都写在这个目录下。其package.json类似步骤2(ps:也可以不这么做,也可以直接所有公共组件写在exposes下,只要组件是个npm包就行。)
{"name": "components","version": "1.0.1","description": "","main": "index.tsx","keywords": [],"author": "","license": "ISC"
}
-
在workspce中创建组件包
在src/exposes/components下新建一个PureButton
文件夹,在这个文件夹中执行npm init -y
生成package.json 文件,主要修改文件中以下字段:
-
name(我们可以用@loctek这个前缀,包名用横线分割,不要用驼峰。)
-
version
-
main(main是这个包的入口)
{"name": "@loctek/pure-button","version": "1.0.0","description": "","main": "index.tsx","types": "index.d.ts","keywords": [],"author": "","license": "ISC","private": true
}
-
书写组件代码
新建index.tsx,书写组件的代码
import styles from './style.module.less';
import type { Props } from './index.d';const PureButton = ({ btnStr = '' }: Props) => {return (<><div className={styles['btn-container']}>{btnStr}</div></>);
};export default PureButton;
-
测验组件是否生效
(这里单纯测试workspace是否生效,实际项目我们采用模块联邦的方式。)
在本地开发中,我们可以在apps的子应用中,在其package.json中添加上面的包,然后在子项目的目录执行pnpm i 或者 pnpm i @loctek/pure-button
命令,这样就会将@loctek/pure-button
组件的软链接添加到子项目的node_modules中供子应用使用。
"dependencies": {//其他包..."@loctek/pure-button": "workspace:*",},
3、利用模块联邦使用公共组件
官网地址:
Module Federation 插件
-
改造基座,导出远程组件
在config/config.ts中使用 mf 字段,这样最终会在打包文件中多出一个remote.js文件。用name起一个名字,remoteHash用于取消打包的hash,library指定打包的文件的模块
export default defineConfig({// 模块联邦mf: {name: 'master',//关闭remote.js的hashremoteHash: false,// qiankun时必须要,window是挂载到window上,默认是varlibrary: { type: 'window', name: 'master' },},//其他配置})
-
子应用注册远程组件
子应用的.umirc.ts中,1、添加mf插件、2、添加mf字段
mf中的remotes指定访问的远程组件地址和他的name,基座最终会被部署到80端口,所以我们的entry就是//
localhost:80/remote.js'
,,remote.js就是会将基座中src/exposes下的文件打包进去。(ps:在本地开发阶段可以写基座项目启动的地址,比如 //localhost:8080/remote.js
)
shared字段填这个子应用用到的远程包,因为我们会将所有的组件写入components,所以直接这样写好就行了。
plugins: ['@umijs/plugins/dist/qiankun', '@umijs/plugins/dist/model', '@umijs/plugins/dist/mf'],//添加 @umijs/plugins/dist/mf'mf: {remotes: [{aliasName: 'masterAppXXX',//一个别名name: 'master', // 对应基座应用的 nameentry: '//localhost:80/remote.js', // 基座应用中导出的共享包的入口},],// 声明共享依赖shared: {'components': {singleton: true,//单例,整个应用只存在一个,防止一个库加载多个版本。eager: false,//控制共享模块的加载时机,默认为false:异步加载,实际用的时候加载;为true时指:同步加载,应用启动就加载,使用于本地开发的时候。requiredVersion: '^1.0.1',},},},
-
使用远程组件
随便在子应用中找个文件用远程的PureButton组件
//impoer MasterApp from '别名/包名'
import MasterApp from 'masterAppXXX/components';
//因为是默认导出,所以需要拿到默认导出的东西后解构
const { PureButton} = MasterApp;
export default function Foo(){return (<><PureButton btnStr="555" /></>)
}
五、应用之间的通信
官网地址:
微前端
useModel
基座
在基座中定义了一些model,并且在app.jsx这个入口文件中导出子应用中需要使用的model
入口文件:app.tsx中导出useQiankunStateForSlave函数。
// 子应用(需要子应用是umi项目)获取主应用的全局状态,需要在app.tsx导出useQiankunStateForSlave供umi使用
interface QiankunState {site: string;
}
export function useQiankunStateForSlave(): QiankunState {const { site } = useModel('site');return {site,//导出site供子应用使用};
}
子应用
在子应用中用useModel('@@qiankunStateFromMaster')
即可拿到导出的数据
import { useModel } from 'umi';export default function HomePage() {// 获取主应用的actionsconst model = useModel('@@qiankunStateFromMaster');console.log(model?.site)return <>...<>}
六、keep-alive
菜单切换时会重新加载子应用,我们能手动来加载子应用的显示隐藏来实现keep-alive的效果,umimax已经为我们提供好了【MicroAppWithMemoHistory】组件,直接用就行。
路由注册时使用这个组件,routes.ts:
export default [ ...['accountService', 'recordCenter'].map((base) => ({path: `/${base}/*`,component: './MicroAppWrapper',})),
]
组件代码:
import { useMemo } from 'react';
import { MicroAppWithMemoHistory } from '@umijs/max';const map = {'accountService': 'qijing','recordCenter': 'haitu',
};function MicroAppWrapper() {const info = useMemo(() => {const pathname = window.location.pathname;const base = pathname.split('/')[1];return {name: map[base],pathname,};}, []);return <MicroAppWithMemoHistory name={info.name} url={info.pathname} />;
}export default MicroAppWrapper;
nginx部署
官网地址:
入门教程 - qiankun
-
可以将子应用和基座部署在同一个server,也可以部署在不同的server下。
一、部署在同一个server
部署在同一个server下,
-
子应用的路由base需要和基座的activeRule保持一致。
-
子应用的entery则是需要与其nginx路径一致
基座注册:
qiankun: {master: {apps: [{name: 'sub-umi-1', //子应用的名称entry: '/sub/umi1/', //子应用的入口地址,nginx配置的目录activeRule: '/app-umi1-history', //子应用的激活规则,指路由sandbox: {strictStyleIsolation: true, //严格样式隔离},},],},},
子应用.umirc.ts
base: '/app-umi2-history', //供基座访问的路由前缀,会和activeRule一样// 用qiankun插件后默认base为包名publicPath: '/sub/umi2/', //资源的前缀,会和nginx中存放的目录保持一次
二、部署在不同的server
部署在不同的server需要为子应用的server添加跨域
nginx.conf内容:
# main主应用server {listen 80;server_name localhost;# CORS 配置add_header Access-Control-Allow-Origin '*' always;add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;add_header Access-Control-Allow-Headers '*' always;add_header Access-Control-Allow-Credentials 'true' always;if ($request_method = 'OPTIONS') {return 204;}location / {root html/main;index index.html index.htm;try_files $uri $uri/ /index.html;}# API 代理配置location ^~ /auth {proxy_pass http://mall-center.dev.springbeetle.top;}location ^~ /perm {proxy_pass http://mall-center.dev.springbeetle.top;}# 处理子应用的代理# location ^~ /qiankun/react {# proxy_pass http://localhost:5173;# }error_page 500 502 503 504 /50x.html;location = /50x.html {root html;}}# umi子应用server {listen 5175;server_name localhost;# # 添加5174全局 CORS 配置add_header Access-Control-Allow-Origin '*' always;add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;add_header Access-Control-Allow-Headers '*' always;add_header Access-Control-Allow-Credentials 'true' always;if ($request_method = 'OPTIONS') {return 204;}location / {root html/qiankun/umi;index index.html index.htm;try_files $uri $uri/ /index.html;}# API 代理配置# location ^~ /auth {# proxy_pass http://mall-center.dev.springbeetle.top;# }error_page 500 502 503 504 /50x.html;location = /50x.html {root html;}}