微前端的概念
微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。
微前端的核心在于 如何拆
和拆完后如何合
systemJS\single-spa(qiankun是基于single-spa)
webpack5的联邦模块实现微前端
微前端解决的问题
- 不同团队(技术栈不同),同时开发一个应用
- 每个团队开发的模块都可以独立开发,独立部署
- 实现增量迁移
如何实现微前端
将一个应用划分成若干个子应用,将子应用打包成一个个模块。当路由切换时加载不同的子应用。这样每个应用都是独立的,技术栈也不用做限制,从而解决前端协同开发问题(子应用需要暴露固定的钩子函数:bootstrap、mount、unmount)
- ifream、webcomponent
- 2018年 single-spa是一个用于前端微服务话的javascript前端解决方案(本身没有处理样式隔离和js隔离)实现了路由劫持和应用加载
- 2019年 qiankun基于single-spa,提供了开箱即用的api(single-spa + sanbox + import-html-entry)做到了 技术栈无关、并且接入简单 实现了样式隔离和 js隔离
- 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 默认的逻辑
- 根据当前路径 查找文件路径
- 加载js文件 方法:script / fetch + eval
- 加载的方法实现
- 加载完成后执行 回调函数
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();
})
}