SystemJS与微前端


SystemJS与微前端

微前端

第一步,解决了什么痛点:

大应用拆分
渐进式技术栈升级

附带优势:
多团队合作,独立部署
技术栈无关

第二步,实现了什么功能:

沙箱:CSS隔离,JS隔离,路由隔离
微应用调度

第三步,如何实现沙箱和微应用调度….

qiankun: 实现沙箱,实现微应用接入配置简化,无痛接入
singleSpa: 实现基于路由进行微应用调度,定义了微应用生命周期
systemJS: 实现动态加载模块

三者是层层递进的关系,qiankun的实现依赖了singleSpa,singleSpa依赖了systemJS

本文将从从最基础的systemJS展开,systemJS的原理

模块规范

前端的工程化中最重要的就是模块化

模块: 实现特定功能的文件,每个文件作用域相互独立,通过暴露接口相互引用

模块化优势: 复用性 可维护性 命名冲突 按需加载

模块化规范: ESM,CJS,UMD,AMD,CMD

就像主机间的沟通需要各种协议,模块化也有各类规范,其实都是一种统一约定

systemJS

systemJS 通用模块加载器,支持多种模块化规范,无window变量污染

使用systemJS可以让使用者,在指定的时机加载模块文件并运行,

systemJS原理简述

  1. 通过JSONP的方式去异步加载指定路径的模块文件。

  2. 同时监听到模块文件加载,完成后执行模块代码,并触发用户注册好的回调。

JSONP是一种跨域请求的方法,通过动态创建script标签实现。Ajax请求普通文件存在跨域问题。

systemJS在微前端中的职责

systemJS的职责非常纯粹,动态加载模块

后文systemJS的使用 章节中,

仅仅是为了展示systemJS的原理,动态加载并调用了react 与 react-dom

看上去似乎普普通通,

但注意这个动态加载时机是可以由使用者控制的,那变成了 按需动态加载

有了systemJS的加入,qiankun拥有了 拆分大应用, 按需动态加载 模块/微应用 的功能。

systemJS与webpack懒加载

动态加载分为两个部分 加载模块文件监听加载结果

二者 加载模块文件 原理相同都是 JSONP

二者 监听加载结果 原理不同,

systemJS的原理是 监听window的属性修改
由此获取模块导出,也因此需要umd格式,模块加载并执行后挂载在window上。

webpack懒加载原理是 window上挂载回调函数供调用
打包时被分离出去的模块,最外层会包裹一层对window上该回调函数的调用。

iframe(待更新)

完美的沙箱机制,自带应用隔离

可以通过postMessage进行通讯

应用之间沟通差,状态无法保存,弹窗只能在iframe中

Web Component。浏览器支持问题

跨域(待更新)

说起JSONP,就想起跨域的知识点了,回头总结一下

看到这里就可以结束了,后面是比较繁琐的内容.仅代表,记一下以防自己以后忘记

systemJS使用

这里假设一个React项目,依赖react,react-dom两个包,实现两个包b的动态加载

// src/index.js
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
const App = () => <div><h1>Hello, World!</h1></div>
root.render(<App />); // 注意这里别写成了 App 不带尖括号的
// webpack.config.js
const path = require('path');
module.exports = (env) => {
    return {
        // 开发模式,方便看到打包后的代码
        mode: 'development',
        output: {
            filename: 'index.js',
            path: path.resolve(__dirname, 'dist'),
            // 指定entry文件打包为systemJS的格式
            libraryTarget: 'system'
        },
        module: {
            rules: [{
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/,
                options: {
                    presets: [
                        [ "@babel/preset-react", {"runtime": "automatic"} ]
                    ]
                }
            }]
        },
        // 不打包react,react-dom,模拟分包
        externals: ['react', 'react-dom']
    }
}

上述文件配置好后,运行webpack,在dist目录下生成index.js

随后再创建一个html文件,如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport">
    <title>Document</title>
</head>
<body>
    <div id="root"></div>
    <!-- 1. 写好模块的路径映射,注意须是umd格式 -->
    <script type="systemjs-importmap">
        {
            "imports":{
                "react-dom":"https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js",
                "react":"https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js"
            }
        }
    </script>
    <!-- 2. 引入systemJS -->
    <script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js"></script>
    <!-- 3. 引入打包好的system模块规范的entry文件 -->
    <script>
        System.import('./index.js')
    </script>
</body>
</html>

浏览器打开index.html文件,hello world 展示完成

systemJS原理

打包为system模块规范的 index.js文件如下

System.register(["react-dom","react"], function(__WEBPACK_DYNAMIC_EXPORT__, __system_context__) {
})
  1. 首先拿到type="systemjs-importmap"中的map模块路径进行解析,拿到依赖模块的CDN路径

  2. System.import('./index.js') 通过JSONP的方式,异步从服务器获取 index.js

  3. 加载完的script都会自动执行, 即执行System.register(执行包的依赖包,执行包),

    将setters依赖包安装函数,execute执行包,挂载在全局对象

  4. 要运行执行包,需要先加载依赖包,promise.all(load(map(setters[])))

  5. 通过JSONP的方式,异步加载依赖["react-dom","react"],监听load事件加载完时触发回调

// 引入index.js 和 依赖包都使用了load方法,此方法为JSONP加载包
function load(id) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = map[id] || id; // 支持cdn的查找
        script.async = true;
        document.head.appendChild(script);
        script.addEventListener('load', function () {
        })
    }) 
}
  1. 加载完的script都会自动执行, umd格式的模块执行完会在window挂载一个具有自身所有API的对象.

  2. systemJS 在load事件后,检查window上是否有新增属性,通过setters将其加入 执行包的执行上下文.

  3. 两个依赖包都加载完成后,开始执行执行包,执行包内部会调用依赖包的API,hello world!

systemJS简单实现

<script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js"></script>
<script>
    System.import('./index.js')
</script>

将index.html中 上述几行替换为下面的内容,效果不变

<script>
    //  直接加载子应用, 导入打包后的包 来进行加载, 采用的规范 system规范
    // 这个地方是自己实现systemjs  
    // 1) systemjs 是如何定义的 先看打包后的结果 System.register(依赖列表,后调函数返回值一个setters,execute)
    // 2) react , react-dom  加载后调用setters 将对应的结果赋予给webpack
    // 3) 调用执行逻辑  执行页面渲染
    // 模块规范 用来加载system模块的
    const newMapUrl = {};
    // 解析 importsMap
    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, value]) => newMapUrl[key] = value)
            }
        })
    }
    // 加载资源
    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(_lastRegister);
            })
        })
    }
    let set = new Set(); // 1)先保存window上的属性
    function saveGlobalProperty() {
        for (let k in window) {
            set.add(k);
        }
    }
    saveGlobalProperty();
    function getLastGlobalProperty() {  // 看下window上新增的属性
        for (let k in window) {
            if (set.has(k)) continue;

            set.add(k);
            return window[k]; // 我通过script新增的变量
        }
    }
    let lastRegister;
    class SystemJs {
        import(id) { // 这个id原则上可以是一个第三方路径cdn
            return Promise.resolve(processScripts()).then(() => {
                // 1)去当前路径查找 对应的资源 index.jsw
                const lastSepIndex = location.href.lastIndexOf('/');
                const baseURL = location.href.slice(0, lastSepIndex + 1);
                if (id.startsWith('./')) {
                    return baseURL + id.slice(2);
                }
                // http  https
            }).then((id) => {
                // 根据文件的路径 来加载资源
                let execute
                return load(id).then((register) => {
                    let { setters, execute:exe } = register[1](() => { })
                    execute = exe
                    // execute 是真正执行的渲染逻辑 
                    // setters 是用来保存加载后的资源,加载资源调用setters
                    //    console.log(setters,execute)
                    return [register[0], setters]
                }).then(([registeration, setters]) => {
                    return Promise.all(registeration.map((dep, i) => {
                        return load(dep).then(() => {
                            const property = getLastGlobalProperty()
                            // 加载完毕后,会在window上增添属性 window.React window.ReactDOM
                            setters[i](property)
                        })
                        // 拿到的是函数,加载资源 将加载后的模块传递给这个setter
                    }))
                }).then(() => {
                    execute();
                })
            })
        }
        register(deps, declare) {
            // 将毁掉的结果保存起来
            lastRegister = [deps, declare]
        }
    }
    const System = new SystemJs()
    System.import('./index.js').then(() => {
        console.log('模块加载完毕')
    })

    // 本质就是先加载依赖列表 再去加载真正的逻辑 
    // (内部通过script脚本加载资源 , 给window拍照保存先后状态)
    // JSONP
    // single-spa 如何借助了 这个system 来实现了模块的加载
</script>

文章作者: 罗紫宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗紫宇 !
  目录