qiankun 内部通过监听浏览器的路由变化事件(如 popstate 和 hashchange)来管理子应用的激活和加载。以下是 qiankun 如何实现这一功能的详细解释:
先了解下一些基本知识点:
popstate 和 hashchange 是浏览器中用于监听 URL 变化的两个不同事件。它们分别用于不同的路由模式(history 模式和 hash 模式)。以下是这两个事件的详细解释和使用场景:
popstate 事件
用途:
- 主要用于监听浏览器历史记录的变化,通常在使用
history模式时使用。 - 当用户点击浏览器的前进或后退按钮,或者通过 JavaScript 调用
history.pushState或history.replaceState时触发。
触发条件:
- 用户点击浏览器的前进或后退按钮。
- 调用
history.pushState或history.replaceState方法。
示例代码:
javascript
window.addEventListener('popstate', (event) => {
console.log('popstate event triggered');
console.log('Current URL:', window.location.href);
// 处理路由变化逻辑
checkActiveApps();
});
hashchange 事件
用途:
- 主要用于监听 URL 的
hash部分的变化,通常在使用hash模式时使用。 - 当 URL 的
hash部分发生变化时触发。
触发条件:
- URL 的
hash部分发生变化。 - 例如,从
http://example.com/#/app1变为http://example.com/#/app2。
示例代码:
javascript
window.addEventListener('hashchange', (event) => {
console.log('hashchange event triggered');
console.log('Current URL:', window.location.href);
// 处理路由变化逻辑
checkActiveApps();
});
startsWith(rule)是 JavaScript 字符串方法,用于检查一个字符串是否以指定的子字符串开头。它返回一个布尔值:如果字符串以指定的子字符串开头,则返回 true,否则返回 false。在 qiankun 中,startsWith(rule) 通常用于匹配当前路由路径和子应用的 activeRule。以下是一个简化的示例,展示如何在 qiankun 中使用 startsWith(rule) 来检查路由是否匹配子应用的 activeRule。
基本用法
ini
const str = "Hello, world!";
const rule = "Hello";
console.log(str.startsWith(rule)); // true
实现细节
-
注册事件监听器:
qiankun在启动时会注册popstate和hashchange事件监听器。- 这些监听器会捕获路由变化,并根据
activeRule来决定哪些子应用需要被激活。
-
检查
activeRule:- 当路由变化时,
qiankun会遍历所有注册的子应用配置,检查当前路由是否匹配任何子应用的activeRule。 - 如果匹配,则激活相应的子应用。
- 当路由变化时,
-
激活子应用:
- 激活子应用包括加载子应用的资源、调用子应用的生命周期钩子(如
bootstrap和mount)等。
- 激活子应用包括加载子应用的资源、调用子应用的生命周期钩子(如
以下是 qiankun 内部监听路由变化的简化代码示例:
scss
let activeApps = [];
let registeredApps = [];
function checkActiveApps() {
const currentPath = window.location.pathname;
registeredApps.forEach(app => {
if (matchPath(currentPath, app.activeRule)) {
if (!activeApps.includes(app)) {
activateApp(app);
activeApps.push(app);
}
} else {
if (activeApps.includes(app)) {
deactivateApp(app);
activeApps = activeApps.filter(a => a !== app);
}
}
});
}
function matchPath(path, rule) {
// 使用 startsWith 检查路径是否以规则开头
return path.startsWith(rule);
}
function activateApp(app) {
// 加载子应用资源
loadApp(app);
// 调用子应用的 bootstrap 和 mount 生命周期钩子
app.bootstrap();
app.mount();
}
function deactivateApp(app) {
// 调用子应用的 unmount 生命周期钩子
app.unmount();
}
function loadApp(app) {
fetch(app.entry)
.then(response => response.text())
.then(html => {
const container = document.querySelector(app.container);
container.innerHTML = html;
});
}
function registerMicroApps(apps) {
registeredApps = apps;
}
function start() {
// 注册事件监听器
window.addEventListener('popstate', checkActiveApps);
window.addEventListener('hashchange', checkActiveApps);
// 初始检查
checkActiveApps();
}
// 示例注册子应用
registerMicroApps([
{
name: 'app1',
entry: 'https://server1.com/app1',
container: '#app1',
activeRule: '/app1',
bootstrap: () => console.log('app1 bootstraped'),
mount: () => console.log('app1 mounted'),
unmount: () => console.log('app1 unmounted'),
},
{
name: 'app2',
entry: 'https://server2.com/app2',
container: '#app2',
activeRule: '/app2',
bootstrap: () => console.log('app2 bootstraped'),
mount: () => console.log('app2 mounted'),
unmount: () => console.log('app2 unmounted'),
},
]);
start();
详细步骤
-
注册子应用:
- 使用
registerMicroApps注册子应用配置。 - 每个子应用的配置包括
name、entry、container和activeRule。
javascriptregisterMicroApps([ { name: 'app1', entry: 'https://server1.com/app1', container: '#app1', activeRule: '/app1', bootstrap: () => console.log('app1 bootstraped'), mount: () => console.log('app1 mounted'), unmount: () => console.log('app1 unmounted'), }, { name: 'app2', entry: 'https://server2.com/app2', container: '#app2', activeRule: '/app2', bootstrap: () => console.log('app2 bootstraped'), mount: () => console.log('app2 mounted'), unmount: () => console.log('app2 unmounted'), }, ]); - 使用
-
启动
qiankun:- 调用
start函数启动qiankun。 start函数会注册popstate和hashchange事件监听器,并进行初始检查。
scssfunction start() { // 注册事件监听器 window.addEventListener('popstate', checkActiveApps); window.addEventListener('hashchange', checkActiveApps); // 初始检查 checkActiveApps(); } start(); - 调用
-
检查激活的子应用:
- 当路由变化时,
checkActiveApps函数会被调用。 - 该函数会遍历所有注册的子应用配置,检查当前路由是否匹配任何子应用的
activeRule。
inifunction checkActiveApps() { const currentPath = window.location.pathname; registeredApps.forEach(app => { if (matchPath(currentPath, app.activeRule)) { if (!activeApps.includes(app)) { activateApp(app); activeApps.push(app); } } else { if (activeApps.includes(app)) { deactivateApp(app); activeApps = activeApps.filter(a => a !== app); } } }); } - 当路由变化时,
-
路径匹配:
matchPath函数使用startsWith(rule)来检查当前路径是否以activeRule开头。
javascriptfunction matchPath(path, rule) { // 使用 startsWith 检查路径是否以规则开头 return path.startsWith(rule); } -
激活子应用:
- 如果路由匹配某个子应用的
activeRule,activateApp函数会被调用。 - 该函数会加载子应用的资源,并调用子应用的
bootstrap和mount生命周期钩子。
scssfunction activateApp(app) { // 加载子应用资源 loadApp(app); // 调用子应用的 bootstrap 和 mount 生命周期钩子 app.bootstrap(); app.mount(); } - 如果路由匹配某个子应用的
-
加载子应用资源:
loadApp函数会根据子应用的entry配置加载子应用的资源。- 例如,加载
https://server1.com/app1/index.html及其相关资源。
inifunction loadApp(app) { fetch(app.entry) .then(response => response.text()) .then(html => { const container = document.querySelector(app.container); container.innerHTML = html; }); }
下面是对 fetch(app.entry).then(response => response.text()).then(html => { ... }) 这段代码的详细解析。
代码解析
ini
fetch(app.entry)
.then(response => response.text())
.then(html => {
const container = document.querySelector(app.container);
container.innerHTML = html;
});
详细步骤
-
发起 HTTP 请求:
fetch(app.entry):使用fetchAPI 发起一个 HTTP GET 请求,app.entry是子应用的入口地址(例如//localhost:8080)。
-
处理响应:
.then(response => response.text()):将响应体解析为文本格式。response.text()返回一个 Promise,解析后的结果是响应的文本内容。
-
处理 HTML 内容:
.then(html => { ... }):将解析后的 HTML 内容传递给回调函数。const container = document.querySelector(app.container):使用document.querySelector选择子应用的挂载容器。app.container是子应用挂载的 DOM 节点选择器(例如#container)。container.innerHTML = html:将解析后的 HTML 内容设置为容器的innerHTML,从而将子应用的 HTML 渲染到指定的容器中。
详细流程图
ini
+-------------------+
| fetch(app.entry) |
| - 发起 HTTP 请求 |
+-------------------+
|
v
+-------------------+
| response => |
| response.text() |
| - 解析响应为文本 |
+-------------------+
|
v
+-------------------+
| html => { |
| const container = |
| document.querySelector(app.container); |
| container.innerHTML = html; |
| - 渲染 HTML 到容器 |
+-------------------+
示例
假设 app.entry 是 //localhost:8080,app.container 是 #container,并且 //localhost:8080 返回以下 HTML 内容:
xml
<!DOCTYPE html>
<html>
<head>
<title>App 1</title>
</head>
<body>
<h1>Hello from App 1</h1>
</body>
</html>
代码执行流程
-
发起 HTTP 请求:
scssfetch('//localhost:8080') -
处理响应:
ini.then(response => response.text())- 假设响应体是上述 HTML 内容。
-
处理 HTML 内容:
ini.then(html => { const container = document.querySelector('#container'); container.innerHTML = html; });container是#container元素。container.innerHTML = html将上述 HTML 内容设置为#container的内容。
可能出现的问题及其解决方案
-
网络请求失败:
-
问题 :
fetch请求失败,例如网络问题或服务器错误。 -
解决方案:
-
使用
catch处理错误。 -
示例:
inifetch(app.entry) .then(response => response.text()) .then(html => { const container = document.querySelector(app.container); container.innerHTML = html; }) .catch(error => { console.error('Fetch error:', error); });
-
-
-
响应格式不正确:
-
问题:响应体不是预期的 HTML 内容。
-
解决方案:
-
检查响应状态码。
-
示例:
inifetch(app.entry) .then(response => { if (!response.ok) { throw new Error('Network response was not ok ' + response.statusText); } return response.text(); }) .then(html => { const container = document.querySelector(app.container); container.innerHTML = html; }) .catch(error => { console.error('Fetch error:', error); });
-
-
-
容器选择器错误:
-
问题 :
app.container选择器不正确或容器不存在。 -
解决方案:
-
确保选择器正确且容器存在。
-
示例:
inifetch(app.entry) .then(response => response.text()) .then(html => { const container = document.querySelector(app.container); if (container) { container.innerHTML = html; } else { console.error('Container not found:', app.container); } }) .catch(error => { console.error('Fetch error:', error); });
-
-
-
安全性问题:
-
问题 :直接设置
innerHTML可能导致 XSS 攻击。 -
解决方案:
-
使用安全的模板引擎或手动处理 HTML 内容。
-
示例:
inifetch(app.entry) .then(response => response.text()) .then(html => { const container = document.querySelector(app.container); if (container) { // 使用安全的模板引擎或手动处理 HTML container.innerHTML = sanitizeHTML(html); } else { console.error('Container not found:', app.container); } }) .catch(error => { console.error('Fetch error:', error); }); function sanitizeHTML(input) { const div = document.createElement('div'); div.textContent = input; return div.innerHTML; }
-
-
通过上述详细解析,你可以更好地理解 fetch(app.entry).then(response => response.text()).then(html => { ... }) 的工作原理和代码实现。
-
卸载子应用:
- 如果路由不再匹配某个子应用的
activeRule,deactivateApp函数会被调用。 - 该函数会调用子应用的
unmount生命周期钩子。
scssfunction deactivateApp(app) { // 调用子应用的 unmount 生命周期钩子 app.unmount(); } - 如果路由不再匹配某个子应用的
总结
startsWith(rule):用于检查字符串是否以指定的子字符串开头,返回布尔值。qiankun中的使用 :在matchPath函数中使用startsWith(rule)来检查当前路由路径是否以子应用的activeRule开头。- 路由变化监听 :通过监听
popstate和hashchange事件,qiankun能够自动检查路由变化,并根据activeRule动态激活和加载子应用。
通过这种方式,qiankun 能够有效地管理子应用的激活和加载,确保在路由变化时正确地加载和卸载子应用。