一、应用隔离
1.1 JS 沙箱实现原理
问题描述
多个子应用运行时,JavaScript 全局变量可能冲突,如何实现完全隔离?
三种沙箱方案对比
方案1:Proxy 沙箱(最优)
class ProxySandbox {
constructor(name) {
this.name = name
this.running = false
// 子应用的沙箱环境
const fakeWindow = Object.create(null)
// 记录变更
this.addedPropsMap = new Map()
this.modifiedPropsMap = new Map()
this.proxyWindow = new Proxy(fakeWindow, {
get: (target, prop) => {
// 优先从沙箱取值
if (prop in target) {
return target[prop]
}
// 某些属性需要从真实 window 取
const rawValue = window[prop]
// 如果是函数,绑定 this 为真实 window
if (typeof rawValue === 'function') {
const boundFunc = rawValue.bind(window)
// 对于构造函数,保留原型链
if (rawValue.prototype) {
Object.setPrototypeOf(boundFunc, rawValue)
}
return boundFunc
}
return rawValue
},
set: (target, prop, value) => {
if (this.running) {
// 记录新增的属性
if (!window.hasOwnProperty(prop)) {
this.addedPropsMap.set(prop, value)
}
// 记录修改的属性
else if (!this.modifiedPropsMap.has(prop)) {
this.modifiedPropsMap.set(prop, window[prop])
}
target[prop] = value
return true
}
return true
},
has: (target, prop) => {
return prop in target || prop in window
},
deleteProperty: (target, prop) => {
if (target.hasOwnProperty(prop)) {
delete target[prop]
return true
}
return true
}
})
}
active() {
if (!this.running) {
this.running = true
}
}
inactive() {
this.running = false
// 清理沙箱环境
this.addedPropsMap.clear()
this.modifiedPropsMap.clear()
}
}
// 使用示例
const sandbox1 = new ProxySandbox('app1')
const sandbox2 = new ProxySandbox('app2')
sandbox1.active()
// 在 sandbox1 中执行代码
with(sandbox1.proxyWindow) {
var count = 1
function test() {
console.log('app1 test')
}
}
sandbox2.active()
// 在 sandbox2 中执行代码
with(sandbox2.proxyWindow) {
var count = 2
function test() {
console.log('app2 test')
}
}
// 两个沙箱互不影响
console.log(window.count) // undefined
方案2:快照沙箱
class SnapshotSandbox {
constructor() {
this.windowSnapshot = {}
this.modifyPropsMap = {}
}
active() {
// 保存当前 window 快照
for (const prop in window) {
if (window.hasOwnProperty(prop)) {
this.windowSnapshot[prop] = window[prop]
}
}
// 恢复上次的修改
Object.keys(this.modifyPropsMap).forEach(prop => {
window[prop] = this.modifyPropsMap[prop]
})
}
inactive() {
// 记录变更
for (const prop in window) {
if (window.hasOwnProperty(prop)) {
if (window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop]
// 还原
window[prop] = this.windowSnapshot[prop]
}
}
}
}
}
// 使用示例(只支持单实例)
const sandbox = new SnapshotSandbox()
sandbox.active()
window.count = 1
sandbox.inactive()
console.log(window.count) // undefined
sandbox.active()
console.log(window.count) // 1
方案3:iframe 沙箱
class IframeSandbox {
constructor() {
this.iframe = document.createElement('iframe')
this.iframe.style.display = 'none'
document.body.appendChild(this.iframe)
this.sandboxWindow = this.iframe.contentWindow
}
execScript(code) {
this.sandboxWindow.eval(code)
}
destroy() {
document.body.removeChild(this.iframe)
}
}
// 使用示例
const sandbox = new IframeSandbox()
sandbox.execScript('var count = 1')
console.log(window.count) // undefined
console.log(sandbox.sandboxWindow.count) // 1
1.2 CSS 样式隔离方案
方案1:Shadow DOM(严格隔离)
// 创建 Shadow DOM
class ShadowCSSIsolation {
constructor(container) {
this.shadowRoot = container.attachShadow({ mode: 'open' })
}
mount(html, css) {
this.shadowRoot.innerHTML = `
<style>${css}</style>
${html}
`
}
unmount() {
this.shadowRoot.innerHTML = ''
}
}
// 使用示例
const container = document.getElementById('app')
const isolation = new ShadowCSSIsolation(container)
isolation.mount(
'<div class="title">标题</div>',
'.title { color: red; }'
)
// 样式完全隔离,不会影响外部
解决 Modal 等弹窗问题
// 劫持 appendChild,将弹窗挂载到 Shadow DOM 内
const originalAppendChild = Node.prototype.appendChild
Node.prototype.appendChild = function(node) {
// 如果是 Modal 等弹窗
if (node.classList?.contains('ant-modal-wrap')) {
// 挂载到 Shadow DOM 内
return shadowRoot.appendChild(node)
}
return originalAppendChild.call(this, node)
}
方案2:Scoped CSS
class ScopedCSS {
constructor(appName) {
this.appName = appName
this.styleNodes = []
}
process(cssText) {
// 给所有选择器加前缀
const prefix = `[data-app="${this.appName}"]`
return cssText.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)/g, (match, selector, connector) => {
// 过滤掉特殊选择器
if (selector.match(/^\s*(@|keyframes|font-face|import|charset)/)) {
return match
}
// 添加前缀
const trimmedSelector = selector.trim()
return `${prefix} ${trimmedSelector}${connector}`
})
}
mount(cssText) {
const processedCSS = this.process(cssText)
const style = document.createElement('style')
style.textContent = processedCSS
document.head.appendChild(style)
this.styleNodes.push(style)
}
unmount() {
this.styleNodes.forEach(node => {
document.head.removeChild(node)
})
this.styleNodes = []
}
}
// 使用示例
const scopedCSS = new ScopedCSS('app1')
scopedCSS.mount(`
.title { color: red; }
.content { font-size: 14px; }
`)
// 处理后的 CSS:
// [data-app="app1"] .title { color: red; }
// [data-app="app1"] .content { font-size: 14px; }
方案3:动态加载/卸载
class DynamicStylesheet {
constructor() {
this.styleNodes = []
}
loadCSS(href) {
return new Promise((resolve, reject) => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
link.onload = () => resolve()
link.onerror = () => reject()
document.head.appendChild(link)
this.styleNodes.push(link)
})
}
loadInlineCSS(css) {
const style = document.createElement('style')
style.textContent = css
document.head.appendChild(style)
this.styleNodes.push(style)
}
unloadAll() {
this.styleNodes.forEach(node => {
if (node.parentNode) {
node.parentNode.removeChild(node)
}
})
this.styleNodes = []
}
}
// 使用示例
const stylesheet = new DynamicStylesheet()
// 子应用挂载时加载样式
await stylesheet.loadCSS('app1.css')
stylesheet.loadInlineCSS('.title { color: red; }')
// 子应用卸载时移除样式
stylesheet.unloadAll()
1.3 全局变量污染处理
// 全局变量白名单
const GLOBAL_VAR_WHITELIST = [
'Vue',
'VueRouter',
'Vuex',
'axios',
'moment',
'lodash',
'_',
'$'
]
class GlobalVarProtector {
constructor(appName) {
this.appName = appName
this.snapshot = {}
}
protect() {
// 保存白名单之外的全局变量
for (const key in window) {
if (!GLOBAL_VAR_WHITELIST.includes(key)) {
this.snapshot[key] = window[key]
}
}
}
restore() {
// 检测新增的全局变量
for (const key in window) {
if (!GLOBAL_VAR_WHITELIST.includes(key) && !(key in this.snapshot)) {
console.warn(`[${this.appName}] 检测到新增全局变量: ${key}`)
delete window[key]
}
}
// 恢复修改的全局变量
for (const key in this.snapshot) {
if (window[key] !== this.snapshot[key]) {
console.warn(`[${this.appName}] 检测到全局变量被修改: ${key}`)
window[key] = this.snapshot[key]
}
}
}
}
// 使用示例
const protector = new GlobalVarProtector('app1')
protector.protect()
// 子应用运行
window.myGlobalVar = 123 // 会被检测到
protector.restore()
1.4 事件监听隔离
class EventListenerManager {
constructor(appName) {
this.appName = appName
this.listeners = []
// 劫持 addEventListener
this.originalAddEventListener = window.addEventListener
this.originalRemoveEventListener = window.removeEventListener
this.hijack()
}
hijack() {
const self = this
window.addEventListener = function(type, listener, options) {
// 记录监听器
self.listeners.push({ type, listener, options })
// 调用原始方法
return self.originalAddEventListener.call(window, type, listener, options)
}
}
restore() {
// 恢复原始方法
window.addEventListener = this.originalAddEventListener
window.removeEventListener = this.originalRemoveEventListener
}
removeAll() {
// 移除所有监听器
this.listeners.forEach(({ type, listener, options }) => {
this.originalRemoveEventListener.call(window, type, listener, options)
})
this.listeners = []
console.log(`[${this.appName}] 已清理 ${this.listeners.length} 个事件监听器`)
}
}
// 使用示例
const eventManager = new EventListenerManager('app1')
// 子应用添加监听
window.addEventListener('resize', handleResize)
window.addEventListener('scroll', handleScroll)
// 子应用卸载时清理
eventManager.removeAll()
eventManager.restore()
二、应用通信
2.1 Props 传递
// 主应用
registerMicroApps([
{
name: 'app1',
entry: '//localhost:8081',
container: '#container',
activeRule: '/app1',
props: {
// 传递数据
user: { name: 'Admin', role: 'admin' },
token: 'xxxx',
// 传递方法
logout: () => { /* 退出登录 */ },
navigate: (path) => { /* 路由跳转 */ },
// 传递工具
api: axios.create({ baseURL: '/api' })
}
}
])
// 子应用接收
export async function mount(props) {
const { user, token, logout, navigate, api } = props
// 使用传递的数据和方法
console.log('用户信息', user)
// 调用主应用方法
logout()
navigate('/home')
// 使用主应用的 API 实例
const res = await api.get('/users')
}
2.2 全局状态管理
// 方案1:qiankun 的 initGlobalState
import { initGlobalState } from 'qiankun'
const actions = initGlobalState({
user: null,
token: null,
theme: 'light'
})
// 主应用监听
actions.onGlobalStateChange((state, prev) => {
console.log('状态变化', state, prev)
})
// 主应用修改
actions.setGlobalState({ user: { name: 'Admin' } })
// 子应用监听
export async function mount(props) {
props.onGlobalStateChange((state, prev) => {
console.log('子应用收到状态变化', state)
})
}
// 子应用修改
props.setGlobalState({ theme: 'dark' })
// 方案2:自定义全局状态
class GlobalStateManager {
constructor() {
this.state = {}
this.listeners = []
}
getState() {
return this.state
}
setState(newState) {
const prevState = { ...this.state }
this.state = { ...this.state, ...newState }
// 通知所有监听者
this.listeners.forEach(listener => {
listener(this.state, prevState)
})
}
onStateChange(listener) {
this.listeners.push(listener)
// 返回取消监听函数
return () => {
const index = this.listeners.indexOf(listener)
if (index > -1) {
this.listeners.splice(index, 1)
}
}
}
}
// 创建全局实例
window.__GLOBAL_STATE__ = new GlobalStateManager()
// 使用
window.__GLOBAL_STATE__.setState({ user: { name: 'Admin' } })
window.__GLOBAL_STATE__.onStateChange((state, prev) => {
console.log('状态变化', state)
})
2.3 EventBus 事件总线
class EventBus {
constructor() {
this.events = {}
}
on(event, handler) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(handler)
}
once(event, handler) {
const onceHandler = (...args) => {
handler(...args)
this.off(event, onceHandler)
}
this.on(event, onceHandler)
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(handler => {
handler(...args)
})
}
}
off(event, handler) {
if (!this.events[event]) return
if (!handler) {
// 移除所有监听
delete this.events[event]
} else {
// 移除指定监听
const index = this.events[event].indexOf(handler)
if (index > -1) {
this.events[event].splice(index, 1)
}
}
}
}
// 创建全局 EventBus
window.__EVENT_BUS__ = new EventBus()
// 主应用
window.__EVENT_BUS__.on('user-login', (user) => {
console.log('用户登录', user)
})
// 子应用
window.__EVENT_BUS__.emit('user-login', { name: 'Admin' })
2.4 LocalStorage/SessionStorage
// 方案1:直接使用(简单但不推荐)
// 主应用
localStorage.setItem('user', JSON.stringify({ name: 'Admin' }))
// 子应用
const user = JSON.parse(localStorage.getItem('user'))
// 方案2:封装统一的 Storage(推荐)
class MicroAppStorage {
constructor(appName) {
this.appName = appName
this.prefix = `micro-app-${appName}-`
}
set(key, value) {
const prefixedKey = this.prefix + key
const data = {
value,
timestamp: Date.now(),
appName: this.appName
}
localStorage.setItem(prefixedKey, JSON.stringify(data))
}
get(key) {
const prefixedKey = this.prefix + key
const data = localStorage.getItem(prefixedKey)
if (!data) return null
try {
const parsed = JSON.parse(data)
return parsed.value
} catch (e) {
return null
}
}
remove(key) {
const prefixedKey = this.prefix + key
localStorage.removeItem(prefixedKey)
}
clear() {
// 清除本应用的所有数据
Object.keys(localStorage).forEach(key => {
if (key.startsWith(this.prefix)) {
localStorage.removeItem(key)
}
})
}
}
// 使用
const storage = new MicroAppStorage('app1')
storage.set('user', { name: 'Admin' })
const user = storage.get('user')
2.5 CustomEvent 自定义事件
// 主应用派发事件
const event = new CustomEvent('micro-app-message', {
detail: {
from: 'main',
type: 'user-update',
data: { name: 'Admin' }
}
})
window.dispatchEvent(event)
// 子应用监听事件
window.addEventListener('micro-app-message', (e) => {
const { from, type, data } = e.detail
console.log('收到消息', from, type, data)
if (type === 'user-update') {
// 处理用户更新
}
})
// 子应用派发事件给主应用
const replyEvent = new CustomEvent('micro-app-message', {
detail: {
from: 'app1',
type: 'notification',
data: { message: '操作成功' }
}
})
window.dispatchEvent(replyEvent)
三、公共依赖处理
3.1 externals 外部化
// 主应用 index.html
<!DOCTYPE html>
<html>
<head>
<title>微前端主应用</title>
<!-- 公共依赖通过 CDN 引入 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4.2.4/dist/vue-router.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.5.0/dist/axios.min.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
// 子应用 webpack 配置
module.exports = {
configureWebpack: {
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'axios': 'axios'
}
}
}
// 子应用正常使用
import { createApp } from 'vue' // 实际从 window.Vue 获取
import axios from 'axios' // 实际从 window.axios 获取
3.2 shared 共享依赖(qiankun)
// 主应用
import { loadMicroApp } from 'qiankun'
loadMicroApp({
name: 'app1',
entry: '//localhost:8081',
container: '#container',
props: {
// 共享依赖
shared: {
vue: window.Vue,
'vue-router': window.VueRouter,
axios: window.axios
}
}
})
// 子应用
export async function mount(props) {
const { shared } = props
// 使用共享依赖
const app = shared.vue.createApp(App)
const router = shared['vue-router'].createRouter({...})
app.use(router)
app.mount('#app')
}
3.3 版本管理策略
// 依赖版本配置文件
// shared-deps.config.js
module.exports = {
dependencies: {
vue: {
version: '3.3.4',
cdn: 'https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js',
global: 'Vue'
},
'vue-router': {
version: '4.2.4',
cdn: 'https://cdn.jsdelivr.net/npm/vue-router@4.2.4/dist/vue-router.global.prod.js',
global: 'VueRouter'
},
axios: {
version: '1.5.0',
cdn: 'https://cdn.jsdelivr.net/npm/axios@1.5.0/dist/axios.min.js',
global: 'axios'
}
}
}
// 主应用动态加载
class SharedDepsLoader {
constructor(config) {
this.config = config
this.loadedDeps = new Set()
}
async load(deps) {
const promises = deps.map(dep => this.loadDep(dep))
await Promise.all(promises)
}
async loadDep(depName) {
if (this.loadedDeps.has(depName)) {
return Promise.resolve()
}
const dep = this.config.dependencies[depName]
if (!dep) {
throw new Error(`未找到依赖配置: ${depName}`)
}
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = dep.cdn
script.onload = () => {
this.loadedDeps.add(depName)
console.log(`${depName}@${dep.version} 加载完成`)
resolve()
}
script.onerror = () => {
reject(new Error(`${depName} 加载失败`))
}
document.head.appendChild(script)
})
}
}
// 使用
const sharedConfig = require('./shared-deps.config.js')
const loader = new SharedDepsLoader(sharedConfig)
await loader.load(['vue', 'vue-router', 'axios'])
四、路由管理
4.1 主应用路由劫持
// 劫持 history API
const originalPushState = window.history.pushState
const originalReplaceState = window.history.replaceState
window.history.pushState = function(...args) {
const result = originalPushState.apply(this, args)
// 触发自定义事件
window.dispatchEvent(new CustomEvent('micro-app-route-change', {
detail: { type: 'pushState', args }
}))
return result
}
window.history.replaceState = function(...args) {
const result = originalReplaceState.apply(this, args)
window.dispatchEvent(new CustomEvent('micro-app-route-change', {
detail: { type: 'replaceState', args }
}))
return result
}
// 监听 popstate
window.addEventListener('popstate', (e) => {
window.dispatchEvent(new CustomEvent('micro-app-route-change', {
detail: { type: 'popstate', state: e.state }
}))
})
4.2 子应用路由同步
// 子应用路由配置
const router = createRouter({
history: createWebHistory('/app1'), // 使用子路径
routes: [...]
})
// 监听路由变化,通知主应用
router.afterEach((to, from) => {
if (window.__POWERED_BY_QIANKUN__) {
// 通知主应用路由变化
window.dispatchEvent(new CustomEvent('子应用路由变化', {
detail: {
app: 'app1',
path: to.path,
query: to.query
}
}))
}
})
4.3 浏览器前进后退
// 主应用处理前进后退
class RouteManager {
constructor() {
this.history = []
this.currentIndex = -1
this.listen()
}
listen() {
window.addEventListener('popstate', (e) => {
const direction = this.getDirection()
console.log('浏览器', direction === 1 ? '前进' : '后退')
// 可以在这里做一些处理
// 比如记录日志、埋点等
})
}
push(path) {
this.currentIndex++
this.history[this.currentIndex] = path
this.history.length = this.currentIndex + 1
}
getDirection() {
// 判断是前进还是后退
// 实际实现会更复杂
return 1 // 1: 前进, -1: 后退
}
}
const routeManager = new RouteManager()
五、性能优化
5.1 预加载子应用
// qiankun 预加载
import { prefetchApps } from 'qiankun'
// 在空闲时间预加载
prefetchApps([
{ name: 'app1', entry: '//localhost:8081' },
{ name: 'app2', entry: '//localhost:8082' }
])
// 自定义预加载策略
class PreloadStrategy {
constructor() {
this.queue = []
this.loading = false
}
add(app) {
this.queue.push(app)
this.process()
}
async process() {
if (this.loading || this.queue.length === 0) {
return
}
this.loading = true
const app = this.queue.shift()
try {
await this.preloadApp(app)
console.log(`${app.name} 预加载完成`)
} catch (error) {
console.error(`${app.name} 预加载失败`, error)
} finally {
this.loading = false
this.process()
}
}
async preloadApp(app) {
// 获取应用资源
const html = await fetch(app.entry).then(res => res.text())
// 解析并预加载 JS/CSS
const scripts = this.extractScripts(html)
const styles = this.extractStyles(html)
await Promise.all([
...scripts.map(src => this.preloadScript(src)),
...styles.map(href => this.preloadStyle(href))
])
}
preloadScript(src) {
return new Promise((resolve) => {
const link = document.createElement('link')
link.rel = 'prefetch'
link.as = 'script'
link.href = src
link.onload = resolve
link.onerror = resolve
document.head.appendChild(link)
})
}
preloadStyle(href) {
return new Promise((resolve) => {
const link = document.createElement('link')
link.rel = 'prefetch'
link.as = 'style'
link.href = href
link.onload = resolve
link.onerror = resolve
document.head.appendChild(link)
})
}
extractScripts(html) {
const scriptRegex = /<script[^>]+src="([^"]+)"/g
const scripts = []
let match
while ((match = scriptRegex.exec(html)) !== null) {
scripts.push(match[1])
}
return scripts
}
extractStyles(html) {
const styleRegex = /<link[^>]+href="([^"]+)"[^>]*rel="stylesheet"/g
const styles = []
let match
while ((match = styleRegex.exec(html)) !== null) {
styles.push(match[1])
}
return styles
}
}
// 使用
const strategy = new PreloadStrategy()
strategy.add({ name: 'app1', entry: '//localhost:8081' })
5.2 子应用缓存
class AppCache {
constructor() {
this.cache = new Map()
}
set(name, resources) {
this.cache.set(name, {
resources,
timestamp: Date.now()
})
}
get(name) {
const item = this.cache.get(name)
if (!item) return null
// 检查是否过期(1小时)
const isExpired = Date.now() - item.timestamp > 3600000
if (isExpired) {
this.cache.delete(name)
return null
}
return item.resources
}
clear(name) {
if (name) {
this.cache.delete(name)
} else {
this.cache.clear()
}
}
}
const appCache = new AppCache()
// 加载应用时先检查缓存
async function loadApp(name, entry) {
let resources = appCache.get(name)
if (!resources) {
// 没有缓存,重新加载
resources = await fetchAppResources(entry)
appCache.set(name, resources)
}
return resources
}
5.3 按需加载
// 路由懒加载
const routes = [
{
path: '/app1',
component: () => import('./views/App1.vue')
},
{
path: '/app2',
component: () => import('./views/App2.vue')
}
]
// 组件懒加载
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<Loading />
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/Heavy.vue')
)
</script>
5.4 资源预取
// 使用 <link rel="prefetch">
function prefetchResource(url, type = 'script') {
const link = document.createElement('link')
link.rel = 'prefetch'
link.as = type
link.href = url
document.head.appendChild(link)
}
// 预取子应用资源
prefetchResource('//localhost:8081/app.js', 'script')
prefetchResource('//localhost:8081/app.css', 'style')
本文档提供了微前端开发中最关键的技术问题解决方案,每个方案都包含详细的原理说明和可运行的代码示例。这些技术点是微前端项目中必须掌握的核心知识。