37

微前端框架chunchao(春潮)开源啦

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzI2NTk2NzUxNg%3D%3D&%3Bmid=2247486430&%3Bidx=1&%3Bsn=6db7f40d0c46c316af78a4eef6144916
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

写在开头

  • 为了让大家更能理解微前端的工作模式,微前端的最佳实践应该还需要探索

  • 乞丐版微前端框架 chunchao 源码开源,仅仅为了让大家学习微前端的工作模式而已,实际项目中,我们有使用Paas模式,web components,git submodule等模式都可以实现微前端,当然业内肯定有独特的、优于这些模式的微前端实现

正式开始

在上篇文章基础上修改,加载子应用方式

  • 首先修改插入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标签,都加载进来了

加载 scriptstyle 标签

样式隔离、沙箱隔离并不是难题,这里不着重实现,可以参考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操作,插入到主应用基座中

7VZNVv6.png!web
  • 一个是相对src,一个是绝对src,为了不改变子应用的打包,我们使用相对src.

faaQria.png!web
  • 此时写一段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中,脚本生效了

nIjiqmM.png!web
  • 为了优雅一些,我们把脚本抽离成单独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.我这里做乞丐版,目前就不做那么正规了,设计框架原则大家不能忘记哈

看样式、脚本都生效了

QbYfie6.png!web
  • 问题也暴露出来了,那么现在我们在子应用中写的样式代码,污染到了基座全局,这样是不可以的,因为每个子应用应该是沙箱环境

    • 如果是script相关的,可以用proxy和defineproperty做处理

    • 如果是样式相关,可以使用shadow dow技术做样式隔离

    • 这里不得不说,web components技术也是可以在某些技术去实现微前端

  • 我们今天主要是实现乞丐版,为了让大家能了解微前端如何工作的,这里也是开放了源码

写在最后

  • 本文gitHub源码仓库: https://github.com/JinJieTan/chunchao ,记得给个 star
  • 我是Peter,架构设计过20万人端到端加密超级群功能的桌面IM软件,现在是一名前端架构师。

    如果你对性能优化有很深的研究,可以跟我一起交流交流,今天这里写得比较浅,但是大部分人都够用,之前问我的朋友,我让它写了一个定时器定时消费队列,最后也能用。哈哈

    https://qianduan.life
    在看
    前端巅峰
    

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK