微前端架构具备以下几个核心价值:
iframe最大的特性就是提供了测觉游原生的使用离方案,不论是样式隔离,is隔离这英问题统规定美解决。但他的最大问题也在于他的隔离性无法破突破,导致应用间上下文无法被共享,随之带来的开发体验,产品体验的问题
iframe 页面没有自己的历史记录,共用父页面的浏览历史,当iframe 页在内部进行跳转时,浏览器地址栏无变化,当浏览器刷新时,无法停留在iframe内部跳转后的页面上,需要用户重新走一遍操作
iframe 页产生的弹窗,一般只能遮罩 iframe 区域。
每次子应用进入都是一次浏览器上下文重建,资源重新加载的过程。
官方文档 https://qiankun.umijs.org/zh/guide
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
创建一个主应用和两个子应用,这里我使用的react18.2.0版本
npx create-react-app micro-main
yarn start
yarn add qiankun
在项目入口处页面注册子应用并启动
import {
registerMicroApps,
start,
addGlobalUncaughtErrorHandler,
addErrorHandler,
} from "qiankun";
registerMicroApps([
{
name: "childOne",
//默认会加载这个html,解析里面的js,动态执行(子应用必须支持跨域)里面,是用fetch去请求的数据
entry: "//localhost:3001",
//挂载到主应用的哪个元素下
container: "#container",
//当我劫持到路由地址为//child-one时,我就把http://localhost:3001这个应用挂载到#container的元素下
activeRule: "/child-one",
},
{
name: "childTwo",
entry: "//localhost:3002",
container: "#container",
activeRule: "/child-two",
},
]);
// 启动 qiankun
start();
import "./App.css";
import Home from "./pages/Home";
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
Link,
} from "react-router-dom";
const Header = () => {
return (
<header>
<div>微前端实践-qiankun</div>
<div>主应用</div>
<nav>
<Link to={"/"}>主应用首页</Link>
<Link to={"/child-one"}>访问子应用1</Link>
<Link to={"/child-two"}>访问子应用2</Link>
</nav>
</header>
);
};
function App() {
return (
<div className="App">
<Router>
<Header />
<Switch>
<Redirect from="/" to="/home" exact />
<Route key="home" path="/home" exact={true} component={Home} />
</Switch>
</Router>
{/* 加载子应用时,将其挂载到该#container元素下 */}
<div id="container"></div>
</div>
);
}
export default App;
同样,我们使用react创建两个子应用
npx create-react-app pro-1
npx create-react-app pro-2
src目录下创建public-path.js文件并在入口文件引入,用于修改运行时的 publicPath
注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
建议使用 history 模式的路由,设置histroy模式路由base,值和它的 activeRule 是一样的
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/child-one' : '/'}>
分别对应bootstrap、mount、unmount、update几个生命周期函数
为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。
let root = null;
function render(props) {
const { container } = props;
// ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
root = ReactDOM.createRoot(
container
? container.querySelector('#root')
: document.getElementById("root")
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log("[react18] react app bootstraped");
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log("[react18] props from main framework", props);
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
if (root) {
root.unmount(); // 卸载组件
root = null;
}
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
这里我们使用react-app-rewired来修改webpack配置,作用是可以在不eject的情况下自定义配置CRA脚手架创建的app
yarn add react-app-rewired --dev
在根目录下创建config-overrides.js文件
const { name } = require("./package");
module.exports = {
webpack: function (config, env) {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = "umd";
// webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
// config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
config.output.globalObject = "window";
return config;
},
devServer: (_) => {
const config = _;
config.headers = {
"Access-Control-Allow-Origin": "*",
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
},
};
然后修改 package.json,scripts中将react-scripts替换成react-app-rewired即可
注意:eject不需要
"scripts": {
"start": "PORT=3001 react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
报错1:Uncaught Error: application 'childOne' died in status LOADING_SOURCE_CODE: [qiankun]: Target container with #container not existed while childOne loading!
主应用container容器不存在导致无法挂载子应用
解决:不要将<div id="container"></div>
放在Router标签内,跟Router同级即可
报错2:Uncaught TypeError: application 'childOne' died in status NOT_MOUNTED: container.getElementById is not a function
container.getElementById不是一个方法
解决:container情况下使用container.querySelector('#root')方式
function render(props) {
const { container } = props;
// ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
root = ReactDOM.createRoot(
container
? container.querySelector('#root')
: document.getElementById("root")
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}
报错3:Uncaught (in promise) TypeError: Failed to fetch
子应用没有运行的情况
qiankun的错误处理问题,当主应用请求不到子应用页面时,如何设置跳转到自定义的404页面,而不是显示错误
解决:
在app.ts中配置qiankun运行时配置,通过qiankun.addGlobalUncaughtErrorHandler()和qiankun.addErrorHandler()添加错误处理
import {
addGlobalUncaughtErrorHandler,
addErrorHandler,
} from "qiankun";
// 异常捕获
addGlobalUncaughtErrorHandler((event) =>
console.log("addGlobalUncaughtErrorHandler err", event)
);
addErrorHandler((err) => {
console.log("addErrorHandler err", err);
});
代码已经上传到github,感兴趣的欢迎clone & star
https://github.com/fozero/hello-qiankun