import(‘http://localhost:8001/js/app.js’)
},
mount() {
// …
},
unmount() {
// …
},
})
},
activeRule: (location) => location.hash === ‘#/vue’
})
这种方式也不靠谱,每次子应用的入口资源文件变了,主应用的代码也得跟着变。还好,我们有第三种方式,那就是在注册子应用的时候,把子应用的入口 URL 写上,由微前端来负责加载资源文件。
registerApplication({
// 子应用入口 URL
pageEntry: ‘http://localhost:8081’
// …
})
“自动”加载资源文件
现在我们来看一下如何自动加载子应用的入口文件(只在第一次加载子应用时执行):
export default function parseHTMLandLoadSources(app: Application) {
return new Promise(async (resolve, reject) => {
const pageEntry = app.pageEntry
// load html
const html = await loadSourceText(pageEntry)
const domparser = new DOMParser()
const doc = domparser.parseFromString(html, ‘text/html’)
const { scripts, styles } = extractScriptsAndStyles(doc as unknown as Element, app)
// 提取了 script style 后剩下的 body 部分的 html 内容
app.pageBody = doc.body.innerHTML
let isStylesDone = false, isScriptsDone = false
// 加载 style script 的内容
Promise.all(loadStyles(styles))
.then(data => {
isStylesDone = true
// 将 style 样式添加到 document.head 标签
addStyles(data as string[])
if (isScriptsDone && isStylesDone) resolve()
})
.catch(err => reject(err))
Promise.all(loadScripts(scripts))
.then(data => {
isScriptsDone = true
// 执行 script 内容
executeScripts(data as string[])
if (isScriptsDone && isStylesDone) resolve()
})
.catch(err => reject(err))
})
}
上面代码的逻辑:
1.利用 ajax 请求子应用入口 URL 的内容,得到子应用的 HTML2.提取 HTML 中 script
style
的内容或 URL,如果是 URL,则再次使用 ajax 拉取内容。最后得到入口页面所有的 script
style
的内容3.将所有 style 添加到 document.head
下,script
代码直接执行4.将剩下的 body 部分的 HTML 内容赋值给子应用要挂载的 DOM 下。
下面再详细描述一下这四步是怎么做的。
一、拉取 HTML 内容
export function loadSourceText(url: string) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = (res: any) => {
resolve(res.target.response)
}
xhr.onerror = reject
xhr.onabort = reject
xhr.open(‘get’, url)
xhr.send()
})
}
代码逻辑很简单,使用 ajax 发起一个请求,得到 HTML 内容。上图就是一个 vue 子应用的 HTML 内容,箭头所指的是要提取的资源,方框标记的内容要赋值给子应用所挂载的 DOM。
二、解析 HTML 并提取 style script 标签内容
这需要使用一个 API DOMParser[13],它可以直接解析一个 HTML 字符串,并且不需要挂到 document 对象上。
const domparser = new DOMParser()
const doc = domparser.parseFromString(html, ‘text/html’)
提取标签的函数 extractScriptsAndStyles(node: Element, app: Application)
代码比较多,这里就不贴代码了。这个函数主要的功能就是递归遍历上面生成的 DOM 树,提取里面所有的 style
script
标签。
三、添加 style 标签,执行 script 脚本内容
这一步比较简单,将所有提取的 style
标签添加到 document.head
下:
export function addStyles(styles: string[] | HTMLStyleElement[]) {
styles.forEach(item => {
if (typeof item === ‘string’) {
const node = createElement(‘style’, {
type: ‘text/css’,
textContent: item,
})
head.appendChild(node)
} else {
head.appendChild(item)
}
})
}
js 脚本代码则直接包在一个匿名函数内执行:
export function executeScripts(scripts: string[]) {
try {
scripts.forEach(code => {
new Function(‘window’, code).call(window, window)
})
} catch (error) {
throw error
}
}
四、将剩下的 body 部分的 HTML 内容赋值给子应用要挂载的 DOM 下
为了保证子应用正常执行,需要将这部分的内容保存起来。然后每次在子应用 mount()
前,赋值到所挂载的 DOM 下。
// 保存 HTML 代码
app.pageBody = doc.body.innerHTML
// 加载子应用前赋值给挂载的 DOM
app.container.innerHTML = app.pageBody
app.mount()
现在我们已经可以非常方便的加载子应用了,但是子应用还有一些东西需要修改一下。
子应用需要做的事情
在 V1 版本里,注册子应用的时候有一个 loadApp()
方法。微前端框架在第一次加载子应用时会执行这个方法,从而拿到子应用暴露的三个方法。现在实现了 pageEntry
功能,我们就不用把这个方法写在主应用里了,因为不再需要在主应用里引入子应用。
但是又得让微前端框架拿到子应用暴露出来的方法,所以我们可以换一种方式暴露子应用的方法:
// 每个子应用都需要这样暴露三个 API,该属性格式为 mini-single-spa-${appName}
window[‘mini-single-spa-vue’] = {
bootstrap,
mount,
unmount
}
这样微前端也能拿到每个子应用暴露的方法,从而实现加载、卸载子应用的功能。
另外,子应用还得做两件事:
1.配置 cors,防止出现跨域问题(由于主应用和子应用的域名不同,会出现跨域问题)2.配置资源发布路径
如果子应用是基于 webpack 进行开发的,可以这样配置:
module.exports = {
devServer: {
port: 8001, // 子应用访问端口
headers: {
‘Access-Control-Allow-Origin’: ‘*’
}
},
publicPath: “//localhost:8001/”,
}
一个完整的示例
示例代码在 examples 目录。
registerApplication({
name: ‘vue’,
pageEntry: ‘http://localhost:8001’,
activeRule: pathPrefix(‘/vue’),
container: $(‘#subapp-viewport’)
})
registerApplication({
name: ‘react’,
pageEntry: ‘http://localhost:8002’,
activeRule:pathPrefix(‘/r