RegistrableApp
name
- string - 必选,微应用的名称,微应用之间必须确保唯一。entry
- string - 必选,微应用的入口。container
- string | HTMLElement - 必选,微应用的容器节点的选择器或者 Element 实例activeRule
- string | (location: Location) => boolean | ArrayLifeCyclest
hash模式 | history模式
要实现前端路由 需要解决两个核心问题:
hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新。
通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:
// 监听路由变化
window.addEventListener('hashchange', onHashChange)
history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新。
history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:
// 监听浏览器前进后退改变URL
window.addEventListener("popstate", onPopState);
"import-html-entry": "^1.12.0",
"path-to-regexp": "^6.2.1",
"qiankun": "^2.7.4"
import { IAppInfo, ILifeCycle } from './types';
import { setAppList, getAppList } from './appList/index';
import { setLifeCycle } from './lifeCycle/index';
import { hackRoute, reRoute } from './route/index';
export const registerMicroApps = (
appList: IAppInfo[],
lifeCycle?: ILifeCycle
) => {
appList && setAppList(appList);
lifeCycle && setLifeCycle(lifeCycle);
};
export const start = () => {
const list = getAppList();
if (!list.length) {
throw new Error('请先注册应用');
}
hackRoute();
reRoute(window.location.href);
};
// appList/index
import { IAppInfo } from '../types';
let appList: IAppInfo[] = [];
export const setAppList = (list: IAppInfo[]): void => {
appList = list;
};
export const getAppList = () => {
return appList;
};
import { ILifeCycle, IInternalAppInfo, IAppInfo } from '../types';
import { EAppStatus } from '../enum';
import { loadHTML } from '../loader'
let lifeCycle: ILifeCycle = {};
export const setLifeCycle = (lifeCycles: ILifeCycle): void => {
lifeCycle = lifeCycles;
};
export const getLifeCycle = () => {
return lifeCycle;
};
// 存储全局生命周期
// 卸载
export const runUnMounted = async (app: IInternalAppInfo) => {
app.status = EAppStatus.UNMOUNTING;
await app.unmounted?.(app);
app.status = EAppStatus.NOT_MOUNTED;
await runLifeCycle('unmounted', app);
};
// 初始化 只执行一次
export const runBootstrap = async (app: IInternalAppInfo) => {
if (app.status !== EAppStatus.LOADED) {
return app;
}
app.status = EAppStatus.BOOTSTRAPING;
await app.bootstrap?.(app);
app.status = EAppStatus.NOT_MOUNTED;
};
// 挂载 可多次执行
export const runMounted = async (app: IInternalAppInfo) => {
app.status = EAppStatus.MOUNTING;
await app.mounted?.(app);
app.status = EAppStatus.MOUNTED;
// 处理对应子应用生命周期
await runLifeCycle('mounted', app);
};
// 加载前
export const runBeforeLoad = async (app: IInternalAppInfo) => {
app.status = EAppStatus.LOADING;
await runLifeCycle('beforeLoad', app);
// 加载子应用资源
// app = await loadHTML(app)
app.status = EAppStatus.LOADED;
};
const runLifeCycle = async (name: keyof ILifeCycle, app: IAppInfo) => {
// lifeCycles - LifeCycles - 可选,全局的微应用生命周期钩子
const fn = lifeCycle[name];
if (fn instanceof Array) {
await Promise.all(fn.map((item) => item(app)));
} else {
await fn?.(app);
}
};
export enum EAppStatus {
NOT_FOUND = 'NOT_FOUND',
NOT_LOADED = 'NOT_LOADED',
LOADING = 'LOADING',
LOADED = 'LOADED',
BOOTSTRAPPING = 'BOOTSTRAPPING',
NOT_MOUNTED = 'NOT_MOUNTED',
MOUNTING = 'MOUNTING',
UNMOUNTED = 'UNMOUNTED',
MOUNTED = 'MOUNTED',
UNMOUNTING = 'UNMOUNTING',
}
export interface IAppInfo {
name: string
entry: string
container: string
activeRule: string
}
export type Lifecycle = (app: IAppInfo) => Promise<any>
export interface ILifecycle {
beforeLoad?: Lifecycle | Lifecycle[]
mounted?: Lifecycle | Lifecycle[]
unmounted?: Lifecycle | Lifecycle
}
export interface IInternalAppInfo extends IAppInfo {
status: EAppStatus
bootstrap?: Lifecycle
mount?: Lifecycle
unmount?: Lifecycle
proxy: any
}
export type EventType = 'hashchange' | 'popstate'
import { EventType } from '../types'
import {
runBoostrap,
runBeforeLoad,
runMounted,
runUnmounted,
} from '../lifeCycle'
import { getAppListStatus } from '../utils'
const capturedListeners: Record<EventType, Function[]> = {
hashchange: [],
popstate: [],
}
// 劫持和 history 和 hash 相关的事件和函数
// 然后我们在劫持的方法里做一些自己的事情
// 比如说在 URL 发生改变的时候判断当前是否切换了子应用
const originalPush = window.history.pushState
const originalReplace = window.history.replaceState
let historyEvent: PopStateEvent | null = null
let lastUrl: string | null = null
export const reroute = (url: string) => {
if (url !== lastUrl) {
const { actives, unmounts } = getAppListStatus()
Promise.all(
unmounts
.map(async (app) => {
await runUnmounted(app)
})
.concat(
actives.map(async (app) => {
await runBeforeLoad(app)
await runBoostrap(app)
await runMounted(app)
})
)
).then(() => {
callCapturedListeners()
})
}
lastUrl = url || location.href
}
const handleUrlChange = () => {
reroute(location.href)
}
export const hackRoute = () => {
window.history.pushState = (...args) => {
originalPush.apply(window.history, args)
historyEvent = new PopStateEvent('popstate')
args[2] && reroute(args[2] as string)
}
window.history.replaceState = (...args) => {
originalReplace.apply(window.history, args)
historyEvent = new PopStateEvent('popstate')
args[2] && reroute(args[2] as string)
}
window.addEventListener('hashchange', handleUrlChange)
window.addEventListener('popstate', handleUrlChange)
window.addEventListener = hackEventListener(window.addEventListener)
window.removeEventListener = hackEventListener(window.removeEventListener)
}
const hasListeners = (name: EventType, fn: Function) => {
return capturedListeners[name].filter((listener) => listener === fn).length
}
const hackEventListener = (func: Function): any => {
return function (name: string, fn: Function) {
if (name === 'hashchange' || name === 'popstate') {
if (!hasListeners(name, fn)) {
capturedListeners[name].push(fn)
return
} else {
capturedListeners[name] = capturedListeners[name].filter(
(listener) => listener !== fn
)
}
}
return func.apply(window, arguments)
}
}
export function callCapturedListeners() {
if (historyEvent) {
Object.keys(capturedListeners).forEach((eventName) => {
const listeners = capturedListeners[eventName as EventType]
if (listeners.length) {
listeners.forEach((listener) => {
// @ts-ignore
listener.call(this, historyEvent)
})
}
})
historyEvent = null
}
}
export function cleanCapturedListeners() {
capturedListeners['hashchange'] = []
capturedListeners['popstate'] = []
}
import { IInternalAppInfo } from '../types'
import { importEntry } from 'import-html-entry'
import { ProxySandbox } from './sandbox'
export const loadHTML = async (app: IInternalAppInfo) => {
const { container, entry } = app
const { template, getExternalScripts, getExternalStyleSheets } =
await importEntry(entry)
const dom = document.querySelector(container)
if (!dom) {
throw new Error('容器不存在')
}
dom.innerHTML = template
await getExternalStyleSheets()
const jsCode = await getExternalScripts()
jsCode.forEach((script) => {
const lifeCycle = runJS(script, app)
if (lifeCycle) {
app.bootstrap = lifeCycle.bootstrap
app.mount = lifeCycle.mount
app.unmount = lifeCycle.unmount
}
})
return app
}
const runJS = (value: string, app: IInternalAppInfo) => {
if (!app.proxy) {
app.proxy = new ProxySandbox()
// @ts-ignore
window.__CURRENT_PROXY__ = app.proxy.proxy
}
app.proxy.active()
const code = `
return (window => {
${value}
return window['${app.name}']
})(window.__CURRENT_PROXY__)
`
return new Function(code)()
}
export class ProxySandbox {
proxy: any
running = false
constructor() {
const fakeWindow = Object.create(null)
const proxy = new Proxy(fakeWindow, {
set: (target: any, p: string, value: any) => {
if (this.running) {
target[p] = value
}
return true
},
get(target: any, p: string): any {
switch (p) {
case 'window':
case 'self':
case 'globalThis':
return proxy
}
if (
!window.hasOwnProperty.call(target, p) &&
window.hasOwnProperty(p)
) {
// @ts-ignore
const value = window[p]
if (typeof value === 'function') return value.bind(window)
return value
}
return target[p]
},
has() {
return true
},
})
this.proxy = proxy
}
active() {
this.running = true
}
inactive() {
this.running = false
}
}
export const prefetch = async (app: IInternalAppInfo) => {
requestIdleCallback(async () => {
const { getExternalScripts, getExternalStyleSheets } = await importEntry(
app.entry
)
requestIdleCallback(getExternalStyleSheets)
requestIdleCallback(getExternalScripts)
})
}
https://qiankun.umijs.org/zh
https://wujie-micro.github.io/doc/guide/
当谈到微前端中的 wujie 和 qiankun 时,它们都是目前国内比较流行的微前端框架。
下面是它们的优缺点对比:
综上所述,wujie 和 qiankun 都有各自的优点和缺点。选择哪个框架取决于具体的项目需求、团队技术栈和开发者的偏好。
除了 wujie 和 qiankun,还有其他一些好用的微前端框架可供选择。以下是其中几个:
Single-SPA 是一个非常流行的微前端框架,它允许开发团队使用不同的技术栈来构建独立的前端应用,并将它们组合成一个整体的应用。它具有灵活性和可扩展性,并且有一个活跃的社区支持。
Piral 是一个基于 Web Components 的微前端框架,它提供了一种模块化的方式来构建和组合前端应用。它具有良好的可扩展性和性能,并且支持多种技术栈。
Luigi 是一个用于构建微前端应用的开源框架,它提供了一种可插拔的方式来组合和集成不同的前端应用。它具有良好的可扩展性和灵活性,并且支持多种技术栈。
Mosaic 是一个基于 Web Components 的微前端框架,它提供了一种模块化的方式来构建和组合前端应用。它具有良好的可扩展性和性能,并且支持多种技术栈。
这些框架都有各自的特点和优势,选择适合自己项目需求和团队技术栈的框架是很重要的。建议在评估这些框架时,考虑其文档质量、社区支持、可扩展性和性能等因素。