前言
大家好这里是阳九,一个中途转行的野路子码农,热衷于研究和手写前端工具.
我的宗旨就是 万物皆可手写
新手创作不易,有问题欢迎指出和轻喷,谢谢
本文章适合有一定React开发经验,使用过qiankun框架,对webpack有一定了解的前端攻城狮,如果没有请绕道恶补基础知识)
本文章内的代码只显示了关键思路,无法真实运行
注册微前端项目
用过乾坤的朋友都知道,我们需要在基座项目中注册微前端,配置对应的参数.
// 注册微前端(乾坤)
registerMicroApps([
{
name: 'react app-1',
entry: '//localhost:3000', // 入口
container: '#microApp_React_1',// 挂载点的id (挂载到哪个div中)
activeRule: '/microReact_1',// 微前端的路由
},
{
name: 'react app-2',
entry: '//localhost:3001',
container: '#microApp_React_2',
activeRule: '/microReact_2',
},
]);
基座项目中准备好对应的微前端容器
// 基座App中添加微前端挂载点
function App() {
return (
<div className="App">
<h1>LZY-QIANKUN-基座</h1>
<div id='microApp_React_1'></div> // 注册挂载点的id
<div id='microApp_React_2'></div>
</div>
);
}
export default App;
子应用导出对应的生命周期函数
1. 首先我们在子应用中导出三个生命周期函数
这三个函数在乾坤中进行调用,用来渲染和切换不同的子应用, 通过传入container,渲染到对应的div节点中
子应用的index.js文件
...
export async function bootstrap() {
console.log('[react18] react app bootstraped');
}
export async function mount(props) {
console.log('[react18] props from main framework', props);
const { container } = props; // 这里的container就是之前注册的div
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
export async function unmount(props) {
const { container } = props;
const root = ReactDOM.createRoot(container);
root.unmount()
}
获取JS,CSS,html资源
理论上,一个完整的前端项目只需要三个文件 bundle.js , index.css , index.html 那么我们的目标就很明确了,获取这三个文件并执行
**1. 我们将两个react项目通过webpack-dev-server运行,一个在3000端口 一个在4000端口 **
npm run start // reactApp port:3000
npm run start // reactApp port 4000
devServer运行时,会生成对应的bundle,html,css文件,只不过并没有写入硬盘,而是存放在内存中并托管到服务器上.
2. 此时我们可以通过ajax,fetch等方法请求到对应的html资源,通过注册时的entry即可获取
const url = app.entry // 就是之前注册时的入口 比如//localhost:3000
const html = await fetch(url).then(res => res.text()) //请求html
3. 此时我们就获取到了类似这样的html字符串
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React App</title>
</head>
<body>
<div id="root"></div>
</body>
<link rel="stylesheet" href="index.css">
<script src="bundle.js"></script>
</html>
4. 之后我们将其用div进行包裹,就可以当作普通的html结构来解析 (多个script,使用数组进行解析)
// 用div包裹
const htmlTemplate = document.createElement('div')
htmlTemplate.innerHTML = html
// 直接用querySelectorAll获取script标签内容
const scripts = htmlTemplate.querySelectorAll('script')
5. 通过script标签上的src,拿到js的路径,拼接上entry,请求即可获得对应的JS代码
// 例子
<script src="./bundle.js"/>
// 获取路径
const script = htmlTemplate.querySelector('script')
const src = script.getAttribute('src')
const path = path.join(url,src) //也就是 localhost:3000/bundle.js
const jsCodeRes = fetch(jsURL).then(res => res.text()) // fetch获取
6. 我们如法炮制,获得css代码,此时我们就已经获得了一个项目的所有部分,js,css,html
通过eval获取微应用导出的三个生命周期方法(重点)
1. UMD模块化规范 我们来看一下什么是UMD模块化规范(以下是UMD模块化方案打包后的webpack代码)
可以看到 用于适配各种模块化方案 用四种不同模式将factory()进行导出
所以我们通过手动构建cjs环境,并插入eval的代码中,将factory函数接出来
(function webpackUniversalModuleDefinition(root, factory) {
if (typeof exports === 'object' && typeof module === 'object') //todo 适配cjs 用module.export 导出factory函数
module.exports = factory();
else if (typeof define === 'function' && define.amd) //todo 适配amd 用define 导出factory函数
define([], factory);
else if (typeof exports === 'object') //todo 适配esm 用export 导出factory函数
exports["react_app-main"] = factory();
else
root["react_app-main"] = factory();//todo 默认导出(在浏览器端root为window)
})((window: Window) => {
// 自己的代码
})
注意: eval方法可以访问外部变量,所以我们直接在外部定义一个module.export变量, 由于模块执行了
module.exports = factory();// factory()也就是导出的三个生命周期方法
这串代码,我们可以在外部接收到三个生命周期方法
// 手动构建cjs环境
const module = { exports: {} }
const exports = module.exports
// 执行上面的UMD模块代码
eval(code)
coneole.log(module.exports) // 获得了三个生命周期方法
执行渲染
此时我们已经有了 三个生命周期方法 ,html结构,css代码 直接执行生命周期方法即可渲染项目
function renderMicroAppByRoute(){
// 这里需要先将上一个App卸载掉
prevApp.unmount()
const {bootstarp,mount,unmount} = module.exports
const container = document.querySelector(app.container) // 根据id获取容器div
await bootstarp()
await app.mount({ container })// 执行渲染,传入对应的div容器
}
劫持路由跳转,渲染微前端
我们通过全局的history对象,劫持路由跳转,判断路由path来渲染不同的子应用
window.addEventListener('popstate', () => { // 监听popState方法
renderMicroAppByRoute()
})
const originPushState = window.history.pushState // 劫持原生的pushState方法
window.history.pushState = (...args) => {
originPushState.apply(window.history, args)
renderMicroAppByRoute()
}
const originReplaceState = window.history.replaceState // 劫持原生replace方法
window.history.replaceState = (...args) => {
originReplaceState.apply(window.history, args)
renderMicroAppByRoute()
}
解决静态资源无法加载的问题
1.我们的基座项目运行在8888端口,子应用请求静态资源也会从这个端口请求
2. 我们需要在子应用里配置一下,子应用根目录下创建public-path.js文件 配置webpack的全局变量
// public-path.js
// webpack构建时,遇到__webpack_public_path__变量会自动拼接 改变资源的请求路径
// 配置后子应用都会从3000端口请求资源
__webpack_public_path__ = 'http://localhost:3000/'
可以看到微应用的请求端口变为了3000
3.我们在子应用初始化时,在全局记录一下这个路径, 然后从这里请求资源
//设置全局变量 让app知道自己是在qiankun里运行
//设置子应用的publicPath,构建时修改
window.__POWER_BY_LZY_QIANKUN__ = true
window.__INJECTED_PUBLIC_PATH_BY_LZY_QIANKUN__ = app.entry + '/'
解决CSS样式冲突
乾坤源码里使用shadow dom解决样式冲突(一个沙箱隔离环境)
在start时进行配置(详细可学习shadow dom和前端沙箱知识)