微前端的作用
- 不同团队、不同技术栈、同时开发一个应用
- 每个团队开发的模块都可以独立开发、独立部署
- 实现增量迁移
实现微前端解决方案
- 怎么将应用拆分
- 怎么进行应用通信
- 怎么进行应用隔离
iframe
优点:
- 微前端最简单解决方案,通过ifram加载子应用
- 通信通过postMessage
- 完美的沙箱机制自带应用隔离
- 非常简单,无需任何改造
- 完美隔离,js、css都是独立的运行环境
- 不限制使用,页面上可以放多个iframe 来组合业务
缺点:
- 每次进来都需要加载,状态不能保留
- 完全的隔离导致与子应用的通信不方便(postMessage、hash等)
- 布局限制,比如子应用里有一个Model,显示的时候只能在那一小块地方展示,不能全屏展示
- 无法进行资源共享,整个应用全量资源加载,加载太慢了
- 用户体验差(弹框只能在iframe中、在内部切换刷新就会丢失状态)
Web Components
- 将前端应用程序分解为自定义HTML元素
- 基于CustomEvent 实现通信
- Shadow DOM天生的作用域隔离
缺点:
- 浏览器支持问题、学习成本、调试困难、修改样式困难等问题
single-spa
- single-spa 通过路由劫持实现应用自由的加载(采用SystemJS),提供应用间公共组件加载及公共业务逻辑处理,子应用需要暴露固定的生命周期钩子 bootstrap、mount、unmount,接入协议
- 基于props主子应用间通信
- 无沙箱机制,需要自己实现js沙箱以及css沙箱
缺点:
- 学习成本、无沙箱机制、需要对原有的应用进行改造、子应用间相同资源重复加载问题
- 没有预加载子模块的功能
实现css隔离的几种方法:
- vue scoped
- shadow-root
- single-spa
实现js隔离的方法:
- iframe
- proxy 隔离 : js沙箱 js运行在沙箱中
- 快照隔离(单实例可以,多实例不友好)
公共依赖
- systemjs -importmap 引入公共依赖
- script标签引入
single-spa 使用
- npm install create-single-spa -g 下载single-spa 包
- create-single-spa 项目名 //创建single-spa项目 parcel (基座)
- XXX-root-config.js 文件中
registerApplication({
name:"@xxx/react",//react/vue/Angluar
app:()=> System.import("@xxx/react"),
activeWhen:(location)=>location.pathname === '/react',
})
同时在index.ejs文件中
<script type="systemjs-importmap">
{
"imports":{
"@xx/root-config":"//localhost:9000/xxx-root-config.js",
"@xx/react":"//localhost:3000/xxx-react.js",
}
}
</script>
single-spa 实现(手写)
在index.html 中
<script src="https://cdn.bootcdn.net/ajax/libs/single-spa/5.9.3/umd/single-spa.min.js"></script>
let { registerApplication,start } = singleSpa
利用esModules
import { registerApplication,start } from "./single-spa/single-spa.js"
single-spa.js
import { registerApplication} from "./application/app.js"
import { start } from "./start.js"
application/app.js
export function registerApplication(){
}
start.js
export function start(){
}
// 实现子应用的注册、挂载、切换、卸载功能
/**
* 子应用状态
*/
// 子应用注册以后的初始状态
const NOT_LOADED = 'NOT_LOADED'
// 表示正在加载子应用源代码
const LOADING_SOURCE_CODE = 'LOADING_SOURCE_CODE'
// 执行完 app.loadApp,即子应用加载完以后的状态
const NOT_BOOTSTRAPPED = 'NOT_BOOTSTRAPPED'
// 正在初始化
const BOOTSTRAPPING = 'BOOTSTRAPPING'
// 执行 app.bootstrap 之后的状态,表是初始化完成,处于未挂载的状态
const NOT_MOUNTED = 'NOT_MOUNTED'
// 正在挂载
const MOUNTING = 'MOUNTING'
// 挂载完成,app.mount 执行完毕
const MOUNTED = 'MOUNTED'
const UPDATING = 'UPDATING'
// 正在卸载
const UNMOUNTING = 'UNMOUNTING'
// 以下三种状态这里没有涉及
const UNLOADING = 'UNLOADING'
const LOAD_ERROR = 'LOAD_ERROR'
const SKIP_BECAUSE_BROKEN = 'SKIP_BECAUSE_BROKEN'
// 存放所有的子应用
const apps = []
/**
* 注册子应用
* @param {*} appConfig = {
* name: '',
* app: promise function,
* activeWhen: location => location.pathname.startsWith(path),
* customProps: {}
* }
*/
export function registerApplication (appConfig) {
apps.push(Object.assign({}, appConfig, { status: NOT_LOADED }))
reroute()
}
// 启动
let isStarted = false
export function start () {
isStarted = true
}
function reroute () {
// 三类 app
const { appsToLoad, appsToMount, appsToUnmount } = getAppChanges()
if (isStarted) {
performAppChanges()
} else {
loadApps()
}
function loadApps () {
appsToLoad.map(toLoad)
}
function performAppChanges () {
// 卸载
appsToUnmount.map(toUnmount)
// 初始化 + 挂载
appsToMount.map(tryToBoostrapAndMount)
}
}
/**
* 挂载应用
* @param {*} app
*/
async function tryToBoostrapAndMount(app) {
if (shouldBeActive(app)) {
// 正在初始化
app.status = BOOTSTRAPPING
// 初始化
await app.bootstrap(app.customProps)
// 初始化完成
app.status = NOT_MOUNTED
// 第二次判断是为了防止中途用户切换路由
if (shouldBeActive(app)) {
// 正在挂载
app.status = MOUNTING
// 挂载
await app.mount(app.customProps)
// 挂载完成
app.status = MOUNTED
}
}
}
/**
* 卸载应用
* @param {*} app
*/
async function toUnmount (app) {
if (app.status !== 'MOUNTED') return app
// 更新状态为正在卸载
app.status = MOUNTING
// 执行卸载
await app.unmount(app.customProps)
// 卸载完成
app.status = NOT_MOUNTED
return app
}
/**
* 加载子应用
* @param {*} app
*/
async function toLoad (app) {
if (app.status !== NOT_LOADED) return app
// 更改状态为正在加载
app.status = LOADING_SOURCE_CODE
// 加载 app
const res = await app.app()
// 加载完成
app.status = NOT_BOOTSTRAPPED
// 将子应用导出的生命周期函数挂载到 app 对象上
app.bootstrap = res.bootstrap
app.mount = res.mount
app.unmount = res.unmount
app.unload = res.unload
// 加载完以后执行 reroute 尝试挂载
reroute()
return app
}
/**
* 将所有的子应用分为三大类,待加载、待挂载、待卸载
*/
function getAppChanges () {
const appsToLoad = [],
appsToMount = [],
appsToUnmount = []
apps.forEach(app => {
switch (app.status) {
// 待加载
case NOT_LOADED:
appsToLoad.push(app)
break
// 初始化 + 挂载
case NOT_BOOTSTRAPPED:
case NOT_MOUNTED:
if (shouldBeActive(app)) {
appsToMount.push(app)
}
break
// 待卸载
case MOUNTED:
if (!shouldBeActive(app)) {
appsToUnmount.push(app)
}
break
}
})
return { appsToLoad, appsToMount, appsToUnmount }
}
/**
* 应用需要激活吗 ?
* @param {*} app
* return true or false
*/
function shouldBeActive (app) {
try {
return app.activeWhen(window.location)
} catch (err) {
console.error('shouldBeActive function error', err);
return false
}
}
// 让子应用判断自己是否运行在基座应用中
window.singleSpaNavigate = true
// 监听路由
window.addEventListener('hashchange', reroute)
window.history.pushState = patchedUpdateState(window.history.pushState)
window.history.replaceState = patchedUpdateState(window.history.replaceState)
/**
* 装饰器,增强 pushState 和 replaceState 方法
* @param {*} updateState
*/
function patchedUpdateState (updateState) {
return function (...args) {
// 当前url
const urlBefore = window.location.href;
// pushState or replaceState 的执行结果
const result = Reflect.apply(updateState, this, args)
// 执行updateState之后的url
const urlAfter = window.location.href
if (urlBefore !== urlAfter) {
reroute()
}
return result
}
}
Module federation
- 通过模块联邦将组件进行打包导出使用
- 共享模块的方式进行通信
- 无css沙箱和js沙箱
缺点:
- 需要webpack5
systemjs
systemjs-importmap 类似于webpack importMap 在imports对象中查找所需要的资源
systemjs实现
1、新建html 主应用文件基座,并用systemjs-importmap 应用公共资源包
<script type="systemjs-importmap" >
//加载cdn资源
{
"imports":{
"react":"https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js",
"react-dom":"https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"
}
}
</script>
<div id="root" ></div>
<script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js" ></script>
<script>
//引入所需要的子应用,导入打包后的包,来进行加载,采用的规范为system规范
//systemjs 是如何定义的 打包后的结果 System.register(依赖列表,回调函数返回值 setters、execute )
//react,react-dom 加载后调用setters 将对应的 结果赋予给webpack
const newMapUrl ={}
//解析importMap
funciton processScripts(){
Array.from(document.querySelectorAll('script')).forEach(script=>{
if(script.type==='systemjs-importmap'){
const imports = JSON.parse(script.innerHTML).imports
Object.entries(imports).forEach(([key,value])=>newMapUrl[key]=value)
}
})
}
// 加载资源
function load(id){
return new Promise((resolve,reject)=>{
const script = document.createElement('script')
script.src=id
script.true = true
document.head.appendChild(script)
script.addEventListener('load',function(){
resolve()
})
})
}
let set = new Set()
function saveGlobalProperty(){
for(let k in window){
if(set.has(k)) continue;
set.add(k)
}
}
saveGlobalProperty()
function getLastGlobalProperty(){
for(let k in window){
if(set.has(k)) continue;
set.add(k)
return window[k]
}
}
let lastRegister;
// 模块规范 用来加载system模块的
class SystemJs{
import(id){ //这个id原则上是一个第三方路径cdn
return Promise.resolve(processScripts()).then(()=>{
// 当前路径查找对应的资源 index.jsw
const lastSepIndex = location.href.lastIndexOf('/')
const baseURL = location.href.slice(0,lastSepIndex+1)
if(id.startWith('./')){
return baseURL +id.slice(2)
}
}).then((id)=>{
// 根据文件的路径,来加载资源
console.log(id)
return laod(id).then((register)=>{
let {setters,execute:exe} = register[1](()=>{})
execute = exe
console.log('文件加载完毕')
return [register[0],setters]
}).then(([registeration,setters])=>{
return Promise.all(registeration.map((dep,i)=>{
return load(dep).then(()=>{
const property = getLastGlobalProperty()
setters[i](property)
})
}))
}).then(()=>{
execute()
})
})
}
register(deps,declare){
lastRegister = [deps,declare]
}
}
const System= new SystemJs
System.import('./index.js').then(()=>{
console.log('模块加载完毕')
})
</script>
qiankun
qiankun 基于shadow-Dom/shadow-root 实现样式隔离的
qiankun 实践
//qiankun 安装
yarn add qiankun # or npm i qiankun -S
//使用
import { loadMicroApp } from 'qiankun';
// 加载微应用
loadMicroApp({
name: 'reactApp',
entry: '//localhost:7100',
container: '#container',
props: {
slogan: 'Hello Qiankun',
},
});
基座的app.js
import { loadMicroApp } from "qiankun"
function App(){
const containerRef =React.createRef();
useEffect(()=>{
loadMicroApp({
name:'m-static',
entry:'//localhost:30000',
container:containerRef.current
})
})
return (
<div className="App">
<BrowserRouter>
<Link to="/react">react应用</Link>
<Link to="/vue">vue应用</Link>
</BrowserRouter>
<div ref={containerRef} ></div>
<div id="container" ></div>
</div>
)
}
基座应用引入 registerApps.js
import { registerMicroApps, start } from 'qiankun'
const loader = (loading)=>{
console.log('加载状态',loading)
}
registerMicroApps([
{
name:'reactApp',//name
entry:'//localhost:10000',//默认启动入口
activeRule:'/react',//当路径为/react 的时候加载
container:"#container" ,// 应用挂在位置
loader,
props:{a:1}//给子应用传参
}
],{
beforeLoad(){
console.log('before Load')
},
beforeMount(){
},
afterMount(){},
beforeUnmount(){},
afterUnmount(){},
})
.env
PORT =10000
WDS_SOCKET_PORT=1000
子应用的index.js
import './public-path.js'
let root;
function render(props){
const container = props.container
root =ReactDOM.creatRoot( container ? container.querySelector('#root') : document.getElementById("root"));
}
//qiankun 提供了一些标识,用于表示当前应用是否在父应用中被引入过
if(!window.__POWERED_BY_QIANKUN__){
render({})
}
// render()
// qiankun 要求应用暴露的格式是umd格式
//修改webpack配置
// npm install @rescripts/cli --force
exprot async function bootstrap(props){}
exprot async function mount(props){
render(props)
}
exprot async function unmount(props){
root.unmount();
}
//如果是vue的话 下面内容放vue.config.js 中
module.export={
webpack:(config)=>{
config.output.libraryTarget = 'umd'
config.output.library = 'react' //打包的格式是umd格式
return config
},
//解决子应用的跨域问题
devServer:(config)=>{
config.headers = {
"Access-control-Allow-Origin":"*"
}
return config
}
}
"scripts":{
"start":"rescripts start",
"build":"rescripts build",
"test":"rescripts test",
"eject":"rescripts eject",
}
在子应用的src下增加 public-path.js
if(window.__POWERED_BY_QIANKUN__){
//eslint-diable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
qiankun 下 新增html 子应用
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" >
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>静态应用</title>
</head>
<body>
<div id="static"></div>
<script entry >
const app =document.getElementById("static");
function render(){
app.innerHTML='static'
}
if(!window.__POWERED_BY_QIANKUN__){
render()
}
window["m-static"] ={
bootstrap:async ()=>{
},
mount:async ()=>{
render()
},
unmount:async ()=>{
}
}
</script>
</body>
</html>
//启动的包
//下个包 http-server --port 30000 --cors