什么是微前端?
个人理解:微前端是一种策略,通过多个团队独立开发部署的方式,共同打造一个web应用的策略。
微前端有什么好处?
- 不限技术
- 独立开发、独立部署
- 独立运行时,每个应用状态隔离,互不影响
项目实战(主应用:vite+vue3;微应用:react、vue2)
创建简单的空架子
首先,按官网项目实战步骤走:
主应用
主应用不区分技术栈,所以这里采用了最近比较火的vite + vue3 + ts
- 先安装
qiankun
yarn add qiankun # 或者 npm i qiankun -S
- 在入口文件main.ts中注册并启动
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'app-react', // 微应用在package.json中的name
entry: '//localhost:3000', // 微应用的路径
container: '#reactContainer', // 微应用挂载的位置
activeRule: '/micro-app-react', // 路由匹配规则(当匹配到此路径时,表示要加载此规则对应的微应用)
},
{
name: 'app-vue',
entry: '//localhost:8080',
container: '#vueContainer',
activeRule: '/micro-app-vue',
},
]);
// 启动 qiankun
start();
- 创建微应用挂载位置的DOM
// App.vue
<template>
<PrimaryMenu />
<router-view></router-view>
<div id="reactContainer"></div> // DOM挂载位置(react)
<div id="vueContainer"></div> // DOM挂载位置(vue2)
</template>
注意
router-view和微应用#reactContainer的挂载位置,一般情况下,如果匹配了微应用的activeRule,就不会显示主应用的router-view
微应用
此示例中用有
webpack的构建项目:react和vue2
react v18.2 + react-router-dom v6.8.1
- 在
src下新建public-path.js文件
if (window.__POWERED_BY_QIANKUN__ && __webpack_public_path__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
此时,eslint会有报错,需要在
package.json文件添加如下配置
"eslintConfig": {
"globals": {
"__webpack_public_path__": true
},
},
- 设置路由模式为
history模式,并设置路由的basename
// router/index.js
import { createBrowserRouter } from 'react-router-dom';
import App from '../App';
import Test from '../components/Test';
const config = [
{
path: '/',
element: <App />,
},
{
path: 'test',
element: <Test />,
},
];
const router = createBrowserRouter(config, { basename: window.__POWERED_BY_QIANKUN__ ? '/micro-app-react' : '/' });
注意
/micro-app-react要和主应用设置的activeRule相对应
- 入口文件
index.js修改
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import './index.css';
import reportWebVitals from './reportWebVitals';
import router from './router';
function render(props) {
const { container } = props;
// 避免根id #root 与其他的 DOM 冲突
const root = ReactDOM.createRoot(container ? container.querySelector('#root') : document.querySelector('#root'));
// 挂载router
root.render(<RouterProvider router={router} />);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
export async function mount(props) {
console.log('[react16] props from main framework', props);
render(props);
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
- 修改
webpack配置
安装插件 @rescripts/cli
npm i -D @rescripts/cli
根目录新增 .rescriptsrc.js:
const { name } = require('./package');
module.exports = {
webpack: (config) => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `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:
- "start": "react-scripts start",
+ "start": "rescripts start",
- "build": "react-scripts build",
+ "build": "rescripts build",
- "test": "react-scripts test",
+ "test": "rescripts test",
- "eject": "react-scripts eject"
vue2
使用的是
vue-cli5.0的版本
-
在
src目录新增public-path.js:if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }eslint报错webpack_public_path__未声明的问题,处理方式同react
-
入口文件
main.js修改,为了避免根 id#app与其他的 DOM 冲突,需要限制查找范围。import './public-path'; // import Vue from 'vue'; import Vue from "vue/dist/vue.esm.js"; // 注意这里:vue-cli5引入Vue的方式不一样 import VueRouter from 'vue-router'; import App from './App.vue'; import routes from './router'; // import store from './store'; Vue.config.productionTip = false; Vue.use(VueRouter); // 注意这里:解决<router-view />标签报错解析不了的问题 let router = null; let instance = null; function render(props = {}) { const { container } = props; router = new VueRouter({ base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/', mode: 'history', routes, }); instance = new Vue({ router, // store, render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } // 独立运行时 if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log('[vue] vue app bootstraped'); } export async function mount(props) { console.log('[vue] props from main framework', props); render(props); } export async function unmount() { instance.$destroy(); instance.$el.innerHTML = ''; instance = null; router = null; } -
打包配置修改(
vue.config.js):const { defineConfig } = require('@vue/cli-service') const { name } = require('./package'); module.exports = defineConfig({ transpileDependencies: true, devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // 把微应用打包成 umd 库格式 // jsonpFunction: `webpackJsonp_${name}`, // 此配置项有问题,暂不清楚咋回事??? }, }, })
好了,到这里已经可以看到效果了
再补充个vite和umi微应用
vite
懒了,不再详细补充了,给一个亲测有效的链接,上边也有demo
小坑:1. 主应用无法加载子应用的图片issues,查看加载路径是主应用路径/xxx.png; 2. 跨域
vite.config.ts
...
server: {
origin: '微应用地址', // 解决主应用无法加载子应用图片问题
cors: true, // 解决跨域
}
umi
umi专门有一个接入qiankun的插件,官网就有实现,链接
小坑:主应用请求子应用的接口404
app.tsx
export const request: RequestConfig = {
prefix: '微应用地址', // 解决主应用请求子应用的接口404问题
...
}
核心功能填充
上边已经把架子搭好了,然后我们还需要使用很多的功能,所以下边我们填充核心功能
路由跳转
主应用
主应用的跳转没什么特别的,我直接用的el-menu
<el-menu
router
:default-active="activeIndex"
mode="horizontal"
:ellipsis="false"
@select="handleSelect"
>
<el-menu-item index="/primary-app">主应用页面</el-menu-item>
<div class="flex-grow" />
<el-menu-item index="/micro-app-react">微应用-react</el-menu-item>
<div class="flex-grow" />
<el-menu-item index="/micro-app-vue">微应用-vue</el-menu-item>
</el-menu>
微应用
正常写的情况下:
import { Link } from 'react-router-dom';
<Link to="/test" >To Test</Link> // 这里直接用Link标签写的,用编程式导航也一样
主应用跳微应用,以及只在当前微应用中跳转时没问题的,但是跳转再跳其他应用(比如主应用)就报错了Uncaught (in promise) DOMException: Failed to execute 'replace' on 'Location': 'http://127.0.0.1:5173undefined' is not a valid URL.
查官网,俩方案,都可以:
注意:不论用哪种方案,跳转的路径必须是全的
history.pushState():mdn 用法介绍
使用
history.pushState()方法时要注意,主应用和微应用都要用这个方法进行路由跳转
主应用
<script setup lang="ts">
import { ref } from 'vue';
const activeIndex = ref('');
const handleSelect = (key: string, keyPath: string[]) => {
window.history.pushState({}, '', key);
};
</script>
<template>
<el-menu
:default-active="activeIndex"
mode="horizontal"
:ellipsis="false"
@select="handleSelect"
>
<el-menu-item index="/primary-app">主应用页面</el-menu-item>
<div class="flex-grow" />
<el-menu-item index="/micro-app-react">微应用-react</el-menu-item>
<div class="flex-grow" />
<el-menu-item index="/micro-app-vue">微应用-vue</el-menu-item>
</el-menu>
</template>
微应用(用react举例)
...
<span onClick={() => {
window.history.pushState({}, '', '/micro-app-react/else');
}}>导航去Else页面</span>
...
- 将主应用的路由实例通过
props传给微应用,微应用这个路由实例跳转。
主应用:
// router.js
...
const router = VueRouter.createRouter({ history: createWebHistory(), routes });
...
// index.js
...
import router from './router.js';
registerMicroApps([
{
name: 'app-react',
entry: '//localhost:3000',
container: '#reactContainer',
activeRule: '/micro-app-react',
// 通过props传给微应用
props: {
pRouter: router,
}
},
{
name: 'app-vue',
entry: '//localhost:8080',
container: '#vueContainer',
activeRule: '/micro-app-vue',
props: {
pRouter: router,
}
},
]);
...
微应用
- react通过context放到全局
// context.js
import { createContext } from 'react';
const pRouterContext = createContext(null);
export {
pRouterContext,
};
// index.js
import { pRouterContext } from './context';
function render(props) {
// pRouter就是主应用传过来的
const { container, pRouter } = props;
const root = ReactDOM.createRoot(container ? container.querySelector('#root') : document.querySelector('#root'));
root.render(
<pRouterContext.Provider value={{ pRouter }}>
<RouterProvider router={router} />
</pRouterContext.Provider>
);
}
// App.js
...
import { pRouterContext } from './context';
<span onClick={() => {
pRouter.push('/micro-app-react/else'); // 注意:路径要写全
}}>导航去Else页面</span>
...
- vue2通过挂载到Vue原型上来实现
main.js
...
function render(props = {}) {
const { container, pRouter } = props;
// 挂载到Vue原型上
Vue.prototype.$pRouter = pRouter;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/micro-app-vue/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
// store,
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
...
App.vue
<template>
<div id="app">
...
<span @click="handleClick">跳转bar页面</span>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
methods: {
handleClick() {
// 使用
this.$pRouter.push('/micro-app-vue/bar');
// window.history.pushState({}, '', '/micro-app-vue/bar') // 也可用window.history.pushState方法
}
}
}
</script>
有博客说在使用父传子路由的方法时,遇到子跳转父,样式加载的问题,我暂时没碰到,有类似问题的可参考
Api
主应用没啥特别的,该咋用杂用。微应用唯一的注意点:baseUrl必须是包含origin的完整的url
api.js
import axios from 'axios';
const baseURL = window.__POWERED_BY_QIANKUN__ ? 'http://localhost:3000/api' : '/api';
const instance = axios.create({
baseURL,
// timeout: 60000,
});
export default instance;
使用
import api from '@/api';
import { useEffect } from 'react';
function Test() {
useEffect(() => {
api.get('/currentUser').then(res => {
console.log(res);
})
}, []);
return <div>测试</div>
}
export default Test;
先这么多,待再补充...
Demo
git地址:github.com/MengfeiCao/…