微前端(1)概念 & systemJS实现

2,431 阅读4分钟

微前端的概念

微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。 微前端的核心在于 如何拆拆完后如何合

image.png

systemJS\single-spa(qiankun是基于single-spa)

webpack5的联邦模块实现微前端

微前端解决的问题

  1. 不同团队(技术栈不同),同时开发一个应用
  2. 每个团队开发的模块都可以独立开发,独立部署
  3. 实现增量迁移

如何实现微前端

将一个应用划分成若干个子应用,将子应用打包成一个个模块。当路由切换时加载不同的子应用。这样每个应用都是独立的,技术栈也不用做限制,从而解决前端协同开发问题(子应用需要暴露固定的钩子函数:bootstrap、mount、unmount)

  1. ifream、webcomponent
  2. 2018年 single-spa是一个用于前端微服务话的javascript前端解决方案(本身没有处理样式隔离和js隔离)实现了路由劫持和应用加载
  3. 2019年 qiankun基于single-spa,提供了开箱即用的api(single-spa + sanbox + import-html-entry)做到了 技术栈无关、并且接入简单 实现了样式隔离和 js隔离
  4. 2020年emp基于module frederation(webpack5) 接入成本低 ,解决了第三方依赖包问题

systemJS

是一个通用的模块加载器,它能在浏览器上动态加载模块。微前端的核心就是加载微应用,我们将应用打包成模块,在浏览器中通过systemJS来加载模块。

1、搭建react开发环境

npm init -y
npm install webpack webpack-cli webpack-dev-server babel-loader
@babel/core @babel/preset-env @babel/preset-react html-webpack-plugin
-D
npm install react react-dom

项目结构

webpack.config.js

微前端的公共模块 必须采用 cdn的方式

生产模式下需要打包成一个模块给别人使用 不用打包index.html、react和react-dom

const path = require('path')
const htmlWebpackPlugin = require('html-webpack-plugin')
module.exports = (env) => {
    return {
        mode: 'development',
        output: {
            filename: 'index.js',
            path: path.resolve(__dirname, 'dist'),
            libaryTarget: env.production? 'system': ''  //生产模式下使用system打包
        },
        module: {
            rules: [
                { test: /\.js$/, use: { loader: "babel-loader" }, exclude: /node_modules/ }
            ]
        },
        plugins: [
            !env.production && new htmlWebpackPlugin({  // 生产模式下需要打包成一个模块给别人使用 不用打包index.html
                template: './public/index.html'
            })
        ].filter(Boolean),
        externals: env.production ? ['react', 'react-dom'] : []  // 微前端的公共模块采用cdn的方式 生产模式下需要打包成一个模块给别人使用 不用打包react和react-dom
    }
}

.babelrc

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ] 
}

浏览器加载模块(dist/index.html)

systemjs-importmap 公共资源配置

<script type="systemjs-importmap">
    {
        "import": {
            react":"https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.pro
duction.min.js",
            "react-dom":"https://cdn.bootcdn.net/ajax/libs/react-
dom/17.0.2/umd/react-dom.production.min.js"
        }
    }
</script>
<div id="root"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js"></script>
<script >
    // 表示可以动态加载模块
    //加载模块的时候会提示加载react和react-dom 会自动在上边加载systemjs-importmap 中配置的要加载的模块
    // 可以加载远程连接
    // 类似AMD的前置依赖  引入index.js的时候需要先加载 react和 react-dom
    System.import('./index.js') 
</script>

2、手动实现system原理

注册了两个模块 这两个模块加载完成后会自动调用setters方法将依赖和回调函数传入

加载完毕后会执行execute 此方法就是index.js 默认的逻辑

  1. 根据当前路径 查找文件路径
  2. 加载js文件 方法:script / fetch + eval
  3. 加载的方法实现
  4. 加载完成后执行 回调函数
    function SystemJS() { }
        function load(id) { // 3、加载的方法实现
            return new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.src = id;
                script.async = true; // 异步加载
                document.head.appendChild(script);

                script.addEventListener('load', function () {
                    console.log('脚本加载完毕 会拿到依赖和回调')
                    let _lastRegister = lastRegister;
                    console.log('lastRegister: ', lastRegister);
                    lastRegister = undefined;  // 加载完清空
                    if (!_lastRegister) { //没有的话  给一个默认 
                        resolve([
                            [],  // 表示没有依赖
                            function (_export) { //表示 没有回调
                                return {
                                    execute: function (_export) {
                                        let obj = getGlobalLastPro();
                                        _export(obj)
                                    }
                                }
                            }
                        ]); // 不是system.js 给默认值
                    }
                    resolve(_lastRegister); // 文件加载完毕后,会将 System.register的参数回传回来
                });
            })
        }
        SystemJS.prototype.import = function (id) {  //1、根据当前路径 查找文件路径
            return new Promise((resolve, reject) => {
                const lastSepIndex = location.href.lastIndexOf('/');
                const baseUrl = location.href.slice(0, lastSepIndex + 1); // 文件的基础路径
                if (id.startsWith('./')) {  // 当前的路径
                    resolve(baseUrl + id.slice(2))
                    console.log('id.slice(2): ', id.slice(2));
                }
            }).then(id => {  // 2、加载js文件  方法:script / fetch + eval
                return load(id).then((registerition) => { // 4、加载完成后执行 回调函数export
                    console.log('registerition: ', registerition);
                    function _export(){}
                    registerition[1](_export)
                    // todo..
                })
            })
        }
        let lastRegister; // 最后加载的模块
        /**
         *  用法 System.register(["react","react-dom"], function(__WEBPACK_DYNAMIC_EXPORT__, __system_context__){}};
});
        */
        SystemJS.prototype.register = function (deps,declare) {  // 将本次注册的依赖和声明 暴露到外部
            lastRegister = [deps,declare]
        }
        let System = new SystemJS();
        System.import('./index.js').then(() => { }) //import返回的是 promise

依赖加载

SystemJS.prototype.import = function (id) {
    // ...
    let e;
    return load(id).then((registration) => {
        function _export(result) {
            console.log(result)
        }
        let declared = registration[1](_export);
        e = declared.execute
        return [registration[0], declared.setters];
    }).then((instantiation) => { // 加载文件后加载依赖文件 
        return Promise.all(instantiation[0].map((dep, i) => {
            var setter = instantiation[1][i];
            return load(dep).then(r => {
                let p = getGlobalLastPro();
                setter(p); // 将属性赋值给webpack中的变量 })
            })
        }))
    }).then(() => {
        e()
    })
}
对比window上新增的属性,返回新添加的属性
let globalMap = new Set()
let saveGlobalPro = () => {
    for (let p in window) {
        globalMap.add(p)
    }
}
saveGlobalPro();
let getGlobalLastPro = () => {
    let result;
    for (let p in window) {
        if (globalMap.has(p)) continue;
        result = window[p]
        result.default = result
        result.__useDefault = true;
    }
    return result
}
实现模块递归加载
function createLoad(id) {
    let e;
    return load(id).then((registration) => { // 加载文件后会将依赖和对应的回调传递过来 
        function _export(key) {
            console.log(key)
        }
        let declared = registration[1](_export); // 获取函数的结果 
        e = declared.execute;
        return [registration[0], declared.setters];
    }).then((deps) => {
        return Promise.all(deps[0].map((dep, i) => {
            let setter = deps[1][i];
            return createLoad(dep).then(() => {
                let p = getGlobalLastPro();
                setter(p);
            })
        }))
    }).then(() => {
        e();
    })
}