写在开头
- 为了让大家更能理解微前端的工作模式,微前端的最佳实践应该还需要探索
- 乞丐版微前端框架
chunchao
源码开源,仅仅为了让大家学习微前端的工作模式而已,实际项目中,我们有使用Paas模式,web components,git submodule等模式都可以实现微前端,当然业内肯定有独特的、优于这些模式的微前端实现
正式开始
- 推荐你先看我之前的几篇文章,这样才能更好的阅读本文
- 如果你有什么问题想跟我交流,可以加入我们的专业微前端交流群/技术交流群
- 往期我的原创推荐:
- 深度:从零编写一个微前端框架
- 微前端框架是怎么导入加载子应用的 【3000字精读】
在上篇文章基础上修改,加载子应用方式
- 首先修改插入dom形式,在请求回来子应用的html内容:
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
console.log(shouldMountApp, 'shouldMountApp');
fetch(shouldMountApp[0].entry)
.then(function (response) {
return response.text();
})
.then(function (text) {
const dom = document.createElement('div');
dom.innerHTML = text;
const subapp = document.querySelector('#subApp-content');
subapp && subapp.appendChild(dom);
});
}
- 直接将子应用的
dom
节点,渲染到基座的对应子应用节点中 - 那么子应用此时除了style、script标签,都加载进来了
加载script
、style
标签
❝样式隔离、沙箱隔离并不是难题,这里不着重实现,可以参考shadow dom,qiankun的proxy隔离代理window实现
❞
- 在qiankun源码中,也是使用了
fetch
去加载·
script、style`标签,然后用key-value形式缓存在一个对象中(方便缓存第二次直接获取),他们的fetch还可以用闭包传入或者使用默认的fetch,这里不做过多源码解析
加载script标签
- 有直接写在html文件里的,有通过script标签引入的(webpack等工程化产物),有async,preload,defer等特殊属性
- 改造子应用1的html文件
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>subapp1</title>
</head>
<body>
<div>subapp1</div>
</body>
<script src="/index.js"></script>
<script>
alert('subapp1')
</script>
</html>
- 此时有了
script
标签,需要加载,根据加载方式,分为html内部的和通过script标签引入的 - 例如:
<script src="/index.js"></script>
<script>
alert('subapp1')
</script>
- 那么首先我们要对这个路径做下处理,子应用entry中有完整url前缀路径,那么我们需要跟这个script标签对src属性拼接处理,然后发送fetch请求获取内容
改造加载APP的函数,拉取script标签(目前只考虑单实例)
`export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
const App = shouldMountApp.pop();
});
`
- 这里有一个坑,如果子应用写的是
script src="/index.js"
,但是读取script标签的src属性,会自动+上主应用的前缀,所以要考虑下如何处理 - 并且针对script标签加载,都做了promise化,这样可以确保拉取成功后再进行dom操作,插入到主应用基座中
- 一个是相对src,一个是绝对src,为了不改变子应用的打包,我们使用相对src.
- 此时写一段js代码,获取下当前的基座的完整url,用正则表达式替换掉即可
const url = window.location.protocol+"//"+window.location.host
`
- 这样就能完整正确获取到script标签的内容了,发送fetch请求,获取内容,然后集体promise化,得到真正的内容:
const res = await Promise.all(paromiseArr);
console.log(res, 'res');
if (res && res.length > 0) {
res.forEach((item) => {
const script = document.createElement('script');
script.innerText = item;
subapp.appendChild(script);
});
}
- 然后插入到subApp子应用的container中,脚本生效了
- 为了优雅一些,我们把脚本抽离成单独function,今天由于简单点,乞丐版,为了给你们学习,所以不讲究太多,都用js写代码了,就不追求稳定和美观了
- 完整的loadApp函数:
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
const App = shouldMountApp.pop();
fetch(App.entry)
.then(function (response) {
return response.text();
})
.then(async function (text) {
const dom = document.createElement('div');
dom.innerHTML = text;
const entryPath = App.entry;
const scripts = dom.querySelectorAll('script');
const subapp = document.querySelector('#subApp-content');
const paromiseArr =
scripts &&
Array.from(scripts).map((item) => {
if (item.src) {
const url = window.location.protocol + '//' + window.location.host;
return fetch(`${entryPath}/${item.src}`.replace(url, '')).then(
function (response) {
return response.text();
}
);
} else {
return Promise.resolve(item.textContent);
}
});
subapp.appendChild(dom);
const res = await Promise.all(paromiseArr);
if (res && res.length > 0) {
res.forEach((item) => {
const script = document.createElement('script');
script.innerText = item;
subapp.appendChild(script);
});
}
});
}
- 抽离脚本处理函数:
- 在loadApp函数中,插入dom后加载脚本
`subapp.appendChild(dom);
handleScripts(entryPath,subapp,dom);`
- 定义脚本处理函数:
export async function handleScripts(entryPath,subapp,dom) {
const scripts = dom.querySelectorAll('script');
const paromiseArr =
scripts &&
Array.from(scripts).map((item) => {
if (item.src) {
const url = window.location.protocol + '//' + window.location.host;
return fetch(`${entryPath}/${item.src}`.replace(url, '')).then(
function (response) {
return response.text();
}
);
} else {
return Promise.resolve(item.textContent);
}
});
const res = await Promise.all(paromiseArr);
if (res && res.length > 0) {
res.forEach((item) => {
const script = document.createElement('script');
script.innerText = item;
subapp.appendChild(script);
});
}
}
- 这样loadApp函数就清晰了
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
const App = shouldMountApp.pop();
fetch(App.entry)
.then(function (response) {
return response.text();
})
.then(async function (text) {
const dom = document.createElement('div');
dom.innerHTML = text;
const entryPath = App.entry;
const subapp = document.querySelector('#subApp-content');
subapp.appendChild(dom);
handleScripts(entryPath, subapp, dom);
});
}
`
开始样式文件处理
- 同理,我们此时要来一个复用,获取所有的style标签,以及link标签,而且是rel="stylesheet"的,这样的我们需要用fetch拉取回来,插入到subapp container中
- 首先在subApp1子应用中+上style标签和样式内容
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>subapp1</title>
<style>
body {
color: red;
}
</style>
</head>
<body>
<div>subapp1</div>
</body>
<script src="/index.js"></script>
<script>
alert('subapp1')
</script>
</html>
`
- 然后在loadApp中加入handleStyles函数
handleScripts(entryPath, subapp, dom);
handleStyles(entryPath, subapp, dom);
- 定义handleStyles函数,20秒解决:
``
export async function handleStyles(entryPath, subapp, dom) {
const arr = [];
const styles = dom.querySelectorAll('style');
const links = Array.from(dom.querySelectorAll('link')).filter(
(item) => item.rel === 'stylesheet'
);
const realArr = arr.concat(styles,links)
const paromiseArr =
arr &&
Array.from(realArr).map((item) => {
if (item.rel) {
const url = window.location.protocol + '//' + window.location.host;
return fetch(`${entryPath}/${item.href}`.replace(url, '')).then(
function (response) {
return response.text();
}
);
} else {
return Promise.resolve(item.textContent);
}
});
const res = await Promise.all(paromiseArr);
if (res && res.length > 0) {
res.forEach((item) => {
const style = document.createElement('style');
style.innerHTML = item;
subapp.appendChild(style);
});
}
}
``
❝这里可以做个promise化,如果加载失败可以报个警告控制台,封装框架大都需要这个,否则无法debug.我这里做乞丐版,目前就不做那么正规了,设计框架原则大家不能忘记哈
❞
看样式、脚本都生效了
- 问题也暴露出来了,那么现在我们在子应用中写的样式代码,污染到了基座全局,这样是不可以的,因为每个子应用应该是沙箱环境
- 如果是script相关的,可以用proxy和defineproperty做处理
- 如果是样式相关,可以使用shadow dow技术做样式隔离
- 这里不得不说,web components技术也是可以在某些技术去实现微前端
- 我们今天主要是实现乞丐版,为了让大家能了解微前端如何工作的,这里也是开放了源码
写在最后
- 本文gitHub源码仓库:
https://github.com/JinJieTan/chunchao
,记得给个star
哦 - 我是Peter,架构设计过20万人端到端加密超级群功能的桌面IM软件,现在是一名前端架构师。
如果你对性能优化有很深的研究,可以跟我一起交流交流,今天这里写得比较浅,但是大部分人都够用,之前问我的朋友,我让它写了一个定时器定时消费队列,最后也能用。哈哈
另外欢迎收藏我的资料网站:前端生活社区:
https://qianduan.life
,感觉对你有帮助,可以右下角点个在看
,关注一波公众号:[前端巅峰
]
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。