微前端源码系列之(一)SystemJs实现流程

1,439 阅读2分钟

为什么需要微前端

1、微前端就是将不同的功能按照不同的维度拆分成多个子应用的。通过主应用来加载这些子应用。微前端的核心在于拆,拆完后在合,实现分而治之

2、微前端解决的问题

  • 不同团队(技术栈不同),同时开发一个应用
  • 每个团队开发的模块可以独立开发,独立不熟
  • 实现增量迁移

3、如何实现微前端

就是将一个应用划分成若干个子应用,将子应用打包成一个个模块。当路径切换时加载不同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了。从而解决了前端协同开发的问题。

4、实现微前端技术方案

  • 采用何种方案进行引用拆分

  • 采用何种方式进行通讯

  • 应用之间如何进行隔离

  • 1)iframe

    • 微前端的最简单的方案,通过 iframe加载子应用
    • 可以通过postMessage进行通讯
    • 完美的沙箱机制自带应用隔离 缺点:用户体验差(弹框只能在iframe中、在内部切换刷新就会丢失状态)
  • 2)web components

    • 将前端应用程序分解为自定义的 HTML 元素
    • 基于 CustomEvent 实现通讯
    • Shadow DOM 天生的作用域隔离 缺点: 浏览器支持问题,学习成本,调试困难、修改样式困难等问题
  • 3)single-spa

    • single-spa 是通过路由解气实现加载(采用 SystemJS), 提供应用间公共组件加载以及公共业务逻辑处理。子应用需要暴露固定的钩子 bootStrap、mount、unMount 接入协议
    • 基于 props 主子应用之间进行通讯 缺点: 学习成本、无沙箱机制、需要对原有的应用进行改造、子应用之间相同资源重复加载问题
  • 4) Module federation

    • 通过模块联邦将组建进行打包导出使用
    • 共享模块之间的方式进行通讯
    • 无 css沙箱和JS沙箱 缺点: 需要webpack5

5、微前端single-spa(底层实现)

  • 乾坤是基于 single-spa实现,single-spa基于systemJs实现,这里我们先看下SystemJs大致实现流程(基于源码不多,直接贴代码)
 <body>
   <!-- systemJs规范 -->
 <script type="systemjs-importmap">
   { // 这里模拟 webpack打包的 react 项目
     "imports": {
       "react-dom": "https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/cjs/react-dom-server-legacy.browser.development.js",
       "react": "https://cdn.bootcdn.net/ajax/libs/react-is/18.2.0/cjs/react-is.development.js"
     }
   }
 </script>

 <script>
   //  1) system 是如何定义的,先看打包后的结果, System.register(参数一:依赖列表, 参数二: 回调函数==返回值==>(setters,execute) )
   //   2) react、react-dom加载后调用 setters将对应的结果赋值给 webpack
   //   3) 执行调用逻辑,执行页面渲染

   //    解析 importMap,生成映射表
   const newMapUrl = {};
   function processScripts() {
     Array.from(document.querySelectorAll("script")).forEach((script) => {
       if (script.type === "systemjs-importmap") {
         const imports = JSON.parse(script.innerHTML).imports;
         Object.entries(imports).forEach(([key, val]) => {
           newMapUrl[key] = val;
         });
       }
     });
   }

   //   加载资源
   function load(id) {
     return new Promise((resolve, reject) => {
       const script = document.createElement("script");
       script.src = newMapUrl[id] || id; // 支持 cdn 查找
       script.async = true;
       document.head.appendChild(script);
       script.addEventListener("load", function () {
         // 先拷贝,在置空
         let _lastRegister = lastRegister;
         lastRegister = undefined;

         resolve(); // 此时会执行代码
       });
     });
   }

   let set = new Set(); // 快照:先保存window上的属性
   function saveGlobalProperty() {
     for (let key in window) {
       set.add(key);
     }
   }
   saveGlobalProperty();

   function getLastGlobalProperty() {
     // 其次看下window上是否添加了新属性
     // 获取时进行对比
     for (let k in window) {
       if (set.has(k)) continue;
       set.add(k);
       return window[k];
     }
   }

   let lastRegister;
   //   模块规范, 用来加载system模块
   class SystemJs {
     import(id) {
       // 这个id原则上可以是一个第三方路径 如:cdn
       return Promise.resolve(processScripts())
         .then(() => {
           //   1、去当前路径查找对应资源
           // console.log(newMapUrl);
           const lasteSepIndex = location.href.lastIndexOf("/");
           const baseUrl = location.href.slice(0, lasteSepIndex + 1);
           if (id.startsWith("./")) {
             return baseUrl + id.slice(2);
           }
         })
         .then((id) => {
           // 根据文件路径加载资源
           // console.log(id);
           let exectue;
           return load(id)
             .then((register) => {
               console.log(register);
               // 这里的第一项是回调函数,执行完返回 setter、execute
               // 这里解构别名
               let { setters, exectue: exec } = register[1](() => {});
               exectue = exec;
               // execute 真正执行的渲染逻辑
               // setters 是用来保存家在后的资源,加载资源调用 setters
               return [register[0], setters];
             })
             .then(([registeration, setters]) => {
               return Promise.all(
                 registeration.map((dep, i) => {
                   load(dep).then(() => {
                     // 拿到是函数, 加载资源,将家在后的模块传递给这个setter
                     // 模块加载完后会在window 上添加属性, window.React  window.ReactDOM

                     // 获取window上最后添加的属性
                     setters[i](getLastGlobalProperty());
                   });
                 })
               );
             })
             .then(() => {
               // 文件加载完之后
               exectue();
             });
         });
     }
     register(deps, declare) {
       console.log("‘文件加载完毕");
       // 将会掉的结果保存
       lastRegister = [deps, declare];
     }
   }

   const System = new SystemJs();
   System.import("./index.js").then((res) => {
     // 这里呢就要返回一个promise
   });

   //  先加载依赖列表, 再去加载真正的逻辑, 内部是通过 创建script 脚本加载资源。 然后就是 给window保存快照,保存先后状态
 </script>
</body>

总结:SystemJs 是基于自己的规范 systemjs-importmap, 创建script 标签进行引入文件(JSONP形式),然后插入到head中,文件加载完之后执行文件。 这里源码已经附上,可以在自己项目打包后的dist文件夹下使用。如有疑问可进行交流关注微信公共号分享更多源码。

qrcode_for_gh_d3fb86ddc322_258 (2).jpg