一、课程目标
- qiankun 整体运行流程
- 微前端实现方案
二、课程大纲
- qiankun 整体流程
- 微前端方案实现
- DIY微前端核心能力
1、微前端方案实现
- 基于 iframe 完全隔离的方案、使用纯的Web Components构建应用
- EMP基于webpack module federation
- qiankun、icestark 自己实现JS以及样式隔离
2、qiankun 整体运行流程
3、DIY 微前端核心能力
3.1 应用注册 registerMicroApps(apps, lifeCycles?)
- 参数
- apps - Array - 必选,微应用的一些注册信息
- lifeCycles - LifeCycles - 可选,全局的微应用生命周期钩子
- 类型
RegistrableApp
name
- string - 必选,微应用的名称,微应用之间必须确保唯一。entry
- string - 必选,微应用的入口。container
- string | HTMLElement - 必选,微应用的容器节点的选择器或者 Element 实例activeRule
- string | (location: Location) => boolean | Array<string | (location: Location) => boolean> - 必选,微应用的激活规则
LifeCyclest
- type Lifecycle = (app: RegistrableApp) => Promise;
- beforeMount - Lifecycle | Array - 可选
- beforeUnmount - Lifecycle | Array - 可选
- afterUnmount - Lifecycle | Array - 可选
3.2 监听路由变化
hash模式 | history模式
如何实现前端路由?
要实现前端路由 需要解决两个核心问题:
- 如何改变 URL 却不引起页面刷新?
- 如何检测 URL 变化了?
下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。
1、hash 实现
hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新。
通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:
- 通过浏览器前进后退改变 URL
- 通过标签改变 URL
- 通过window.location改变URL
这几种情况改变 URL 都会触发 hashchange 事件
// 监听路由变化
window.addEventListener('hashchange', onHashChange)
2、history 实现
history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新。
history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:
- 通过浏览器前进后退改变 URL 时会触发 popstate 事件,
- 通过pushState/replaceState或标签改变 URL 不会触发 popstate 事件。好在我们可以拦截 pushState/replaceState的调用和标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。
// 监听浏览器前进后退改变URL
window.addEventListener("popstate", onPopState);
3.3 路由劫持
- 路由变化时匹配子应用
- 执行子应用生命周期
- 加载子应用
3.4 生命周期
- 主应用
- beforeLoad: 挂载子应用前。
- mounted: 挂载子应用后。
- ummounted: 卸载子应用后。
- 子应用
- bootstrap: bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
- mount: 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法。
- unmount: 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例。
3.5 资源加载
- 加载样式表
- 加载js资源
- 执行js代码
3.6 预加载
具体实现
依赖包
"import-html-entry": "^1.12.0",
"path-to-regexp": "^6.2.1",
"qiankun": "^2.7.4"
start入口
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
// appList/index
import { IAppInfo } from '../types';
let appList: IAppInfo[] = [];
export const setAppList = (list: IAppInfo[]): void => {
appList = list;
};
export const getAppList = () => {
return appList;
};
存储lifeCycle 以及生命周期方法的实现
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);
}
};
TS相关类型-枚举
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',
}
TS相关类型
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'] = []
}
loader 加载器
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)()
}
ProxySandbox
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)
})
}
4、qiankun 和 wujie 对比
https://qiankun.umijs.org/zh
https://wujie-micro.github.io/doc/guide/
当谈到微前端中的 wujie 和 qiankun 时,它们都是目前国内比较流行的微前端框架。
下面是它们的优缺点对比:
wujie 的优点:
- 简单易用:wujie 提供了简单易用的 API,使得构建微前端应用变得更加容易。
- 灵活性:wujie 允许开发团队使用不同的技术栈来构建独立的微前端应用,从而提供了更大的灵活性。
- 性能优化:wujie 通过按需加载和缓存机制来优化性能,减少了加载时间和带宽消耗。
wujie 的缺点:
- 社区支持相对较少:相对于 qiankun,wujie 的社区支持相对较少,这可能会导致在解决问题时遇到一些困难。
- 文档相对不完善:wujie 的文档相对不完善,可能需要开发者花费更多的时间来理解和使用该框架。
qiankun 的优点:
- 成熟稳定:qiankun 是一个成熟稳定的微前端框架,已经在许多生产环境中得到验证。
- 强大的生态系统:qiankun 有一个庞大的社区支持和活跃的开发者社区,提供了丰富的插件和工具,使得开发更加便捷。
- 性能优化:qiankun 通过资源预加载、按需加载和缓存机制来优化性能,提供了更好的用户体验。
qiankun 的缺点:
- 学习曲线较陡峭:相对于 wujie,qiankun 的学习曲线可能较陡峭,需要开发者花费一些时间来学习和理解该框架。
- 对主应用的侵入性较大:qiankun 对主应用的侵入性较大,需要在主应用中进行一些特定的配置和修改。
综上所述,wujie 和 qiankun 都有各自的优点和缺点。选择哪个框架取决于具体的项目需求、团队技术栈和开发者的偏好。
拓展
除了 wujie 和 qiankun,还有其他一些好用的微前端框架可供选择。以下是其中几个:
1. Single-SPA:
Single-SPA 是一个非常流行的微前端框架,它允许开发团队使用不同的技术栈来构建独立的前端应用,并将它们组合成一个整体的应用。它具有灵活性和可扩展性,并且有一个活跃的社区支持。
2. Piral:
Piral 是一个基于 Web Components 的微前端框架,它提供了一种模块化的方式来构建和组合前端应用。它具有良好的可扩展性和性能,并且支持多种技术栈。
3. Luigi:
Luigi 是一个用于构建微前端应用的开源框架,它提供了一种可插拔的方式来组合和集成不同的前端应用。它具有良好的可扩展性和灵活性,并且支持多种技术栈。
4. Mosaic:
Mosaic 是一个基于 Web Components 的微前端框架,它提供了一种模块化的方式来构建和组合前端应用。它具有良好的可扩展性和性能,并且支持多种技术栈。
这些框架都有各自的特点和优势,选择适合自己项目需求和团队技术栈的框架是很重要的。建议在评估这些框架时,考虑其文档质量、社区支持、可扩展性和性能等因素。