简单搭建一个微前端项目(qiankun)
其实就是按照官方文档来搭建的,不过也踩了一部分的坑,零零碎碎搜了一些解决问题的办法,做一个记录。
现公司项目都是基于微前端开发的,所以才要去学习,其实搭建好了,后面写项目时感觉和一般项目没有什么区别。
项目搭建
创建三个测试 Demo
- micro-base: 主应用
npx create-react-app micro-base --template typescript - micro-react: react 创建的子应用
npx create-react-app micro-react --template typescript - micro-vue: vue2 创建的子应用
vue create micro-vue
配置
- 首先将创建的项目根节点 id 更改了(防止冲突),一般使用脚手架创建的项目根节点。
- 在每个项目根目录下新建 .env 文件,设置各个项目的 port:
PORT=xxx。当然,你也可以使用其他方法设置项目的启动端口。
主应用配置
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { registerMicroApps, start } from 'qiankun';
import './assets/css/index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// 配置
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:3011',
container: '#reactContainer',
activeRule: '/micro-react',
},
{
name: 'vue app',
entry: '//localhost:3012',
container: '#vueContainer',
activeRule: '/micro-vue',
},
]);
start();
// 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();
react 子应用配置
按照官方文档,先在 src 下新增 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
src/index.tsx
import './public-path';
import React from 'react';
import ReactDOMClient, { Root } from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
import App from './App';
let root: Root | null = null;
function render(props: any) {
const { container } = props;
root = ReactDOMClient.createRoot(
container ? container.querySelector('#microReactRoot') as HTMLElement : document.getElementById('microReactRoot') as HTMLElement
);
root.render(
<React.StrictMode>
<Router basename={window.__POWERED_BY_QIANKUN__ ? '/micro-react' : '/'}>
<App />
</Router>
</React.StrictMode>
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
export async function mount(props: any) {
console.log('[react16] props from main framework', props);
render(props);
}
export async function unmount(props: any) {
console.log(props);
root?.unmount();
}
防止 window.__POWERED_BY_QIANKUN__ 报错,在src下新增 types/index.d.ts
export {};
declare global {
interface Window {
__POWERED_BY_QIANKUN__: string;
}
}
接着就是修改 webpack 的配置,按照官网,首先安装 @rescripts/cli,再在根目录下新建 .rescriptsrc.js
const { name } = require('./package');
module.exports = {
webpack: (config) => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
// config.resolve.extensions = ['.js', '.jsx', '.ts', '.tsx'];
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": {
"start": "rescripts start",
"build": "rescripts build",
"test": "rescripts test"
},
vue2 子应用配置
按照官网,依然是在 src 目录下新增 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
src/main.js
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import VueRouter from 'vue-router'
import store from './store'
import './public-path'
Vue.use(VueRouter)
Vue.config.productionTip = false
let router = null
let instance = null
function render() {
!router && (router = new VueRouter({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? 'micro-vue' : '/',
routes
}))
!instance && (instance = new Vue({
router,
store,
render: h => h(App)
}).$mount('#microVueRoot'))
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
export async function bootstrap(props) {
console.log('[vue] vue app bootstraped', props)
}
export async function mount() {
console.log('[vue] props from main framework')
// 渲染
render()
}
export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
router = null
}
然后再更改路由配置,因为按照官网的配置了,所以要改动一下路由的初始配置 src/router/index.js:
// import Vue from 'vue'
// import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
// Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
// const router = new VueRouter({
// mode: 'history',
// base: process.env.BASE_URL,
// routes
// })
export default routes
再修改打包配置,根目录下新增 vue.config.js:
const { name } = require('./package.json')
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*'
}
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`
}
}
}
至此,初始化的配置就已经结束了,为了让项目看起来更加规范一些,所以加上一些细节的调整。
继续配置
主应用
- 安装 antd:
yarn add antd - 安装 react-router-dom:
yarn add react-router-dom
在 src 目录下新增 assets/css/index.css,引入 antd 的样式:
@import '~antd/dist/antd.css';
在 src 目录下新增 layout 文件夹,存放项目的 layout 组件
src/layout/index.tsx
import {
DesktopOutlined,
PieChartOutlined
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Breadcrumb, Layout, Menu } from 'antd';
import React, { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import './index.css';
const { Header, Content, Footer, Sider } = Layout;
type MenuItem = Required<MenuProps>['items'][number];
function getItem(
label: React.ReactNode,
key: React.Key,
icon?: React.ReactNode,
children?: MenuItem[],
): MenuItem {
return {
key,
icon,
children,
label,
} as MenuItem;
}
// 配置左侧菜单
const items: MenuItem[] = [
getItem(<Link to="/micro-vue">vue2 子应用</Link>, '/micro-vue', <PieChartOutlined />),
getItem(<Link to="/micro-react">react 子应用</Link>, '/micro-react', <DesktopOutlined />)
];
const LayoutIndex: React.FC = () => {
const { pathname } = useLocation();
// 控制菜单收缩展开
const [collapsed, setCollapsed] = useState(false);
// 初始默认选中的菜单
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [breadcrumb, setBreadcrumb] = useState('')
useEffect(() => {
// 根据路由来设置当前选中的菜单,当然,一旦跳到子路由了菜单就无法选中了
// 只是搭建一个初始的项目,所以没有优化这些细节
setSelectedKeys([pathname]);
const _breadcrumb = pathname === '/micro-vue' ? 'vue2' : 'react';
setBreadcrumb(_breadcrumb);
}, [pathname])
return (
<Layout className="container" style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={collapsed} onCollapse={value => setCollapsed(value)}>
<div className="logo" />
<Menu theme="dark" selectedKeys={selectedKeys} mode="inline" items={items} />
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background" style={{ padding: 0 }} />
<Content style={{ margin: '0 16px' }}>
<Breadcrumb style={{ margin: '16px 0' }}>
<Breadcrumb.Item>{breadcrumb}</Breadcrumb.Item>
</Breadcrumb>
<div className="site-layout-background" style={{ padding: 24, minHeight: 360 }}>
{/* 微应用容器 */}
<div id="reactContainer"></div>
<div id="vueContainer"></div>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>微前端 Demo 主应用</Footer>
</Layout>
</Layout>
);
};
export default LayoutIndex;
src/layout/index.css
.container .logo {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.3);
}
.site-layout .site-layout-background {
background: #fff;
}
最后就是引入使用 src/App.tsx:
import React from 'react';
import LayoutIndex from './layouts';
import {BrowserRouter as Router} from 'react-router-dom';
function App() {
return (
<div>
<Router>
<LayoutIndex />
</Router>
</div>
);
}
export default App;
至此,主应用差不多就这样了。
react 子应用
就是新增两个测试的页面,这个就比较简单,可以自行添加,唯一不一样的就是如果建项目使用的是 react-router-dom v6 的话,可能就和其他版本的配置有一点差别。
src/App.tsx:
import React from 'react';
import { Link, Routes, Route } from 'react-router-dom';
import About from './pages/About';
import Home from './pages/Home';
function App() {
return (
<div className="App">
<p>React 子应用</p>
<Link to="/">Home</Link> |
<Link to="/about">About</Link>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
);
}
export default App;
vue 子应用
本来就有两个现成的页面,所以就不配置其他的了,直接使用。
最终效果
至此就结束了,如果遇到什么问题,可以一起讨论。