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

写在开头

  • 为了让你们更能理解微前端的工做模式,微前端的最佳实践应该还须要探索
  • 乞丐版微前端框架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实现html

前端

  • 在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.我这里作乞丐版,目前就不作那么正规了,设计框架原则你们不能忘记哈webpack

git

看样式、脚本都生效了

  • 问题也暴露出来了,那么如今咱们在子应用中写的样式代码,污染到了基座全局,这样是不能够的,由于每一个子应用应该是沙箱环境
  • 若是是script相关的,能够用proxy和defineproperty作处理
  • 若是是样式相关,可使用shadow dow技术作样式隔离
  • 这里不得不说,web components技术也是能够在某些技术去实现微前端
  • 咱们今天主要是实现乞丐版,为了让你们能了解微前端如何工做的,这里也是开放了源码

写在最后

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

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

    另外欢迎收藏个人资料网站:前端生活社区:https://qianduan.life,感受对你有帮助,能够右下角点个在看,关注一波公众号:[前端巅峰]web

相关文章
相关标签/搜索