single-spa示例
single-spa 示例
项目地址
搭建基座项目
在命令行中运行以下命令
pnpm create vue@latest
跟着步骤搭建项目
基座应用改造
删除无用文件,仅保留 App.vue,删除 router/index.ts 中无用路由,删除 src/main.css 中无用样式
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
});
export default router;
/* src/main.css */
@import './base.css';
#app {
width: 100%;
height: 100vh;
}
安装个性化插件
安装 ui 框架和 sass 处理器
pnpm install ant-design-vue@4.x @ant-design/icons-vue --save
pnpm install sass sass-loader --save-dev
改造 App.vue
<script setup lang="ts">
import { ref, type CSSProperties, VueElement, reactive } from 'vue';
import type { ItemType, MenuProps } from 'ant-design-vue';
import router from './router';
const layoutStyle: CSSProperties = {
height: '100%',
};
const headerStyle: CSSProperties = {
textAlign: 'center',
lineHeight: '60px',
color: '#fff',
};
const contentStyle: CSSProperties = {};
const siderStyle: CSSProperties = {};
const selectedKeys = ref<string[]>(['Vue2']);
const openKeys = ref<string[]>(['1']);
function getItem(
label: VueElement | string,
key: string,
icon?: any,
children?: ItemType[],
type?: 'group'
): ItemType {
return {
key,
icon,
children,
label,
type,
} as ItemType;
}
const items: ItemType[] = reactive([
getItem('子项目', '1', null, [
getItem('Vue', 'vue', null, [], 'group'),
getItem('React', 'react', null, [], 'group'),
]),
]);
const handleClick: MenuProps['onClick'] = (e) => {
router.push({
path: `/${e.key}`,
});
};
</script>
<template>
<a-layout :style="layoutStyle">
<a-layout-sider :style="siderStyle">
<a-menu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
mode="inline"
:items="items"
@click="handleClick"
></a-menu
></a-layout-sider>
<a-layout>
<a-layout-header :style="headerStyle">子项目</a-layout-header>
<!-- #microApp 为子项目挂载/渲染容器 -->
<a-layout-content :style="contentStyle" id="microApp"></a-layout-content>
</a-layout>
</a-layout>
</template>
<style lang="scss">
.ant-layout-content {
overflow-y: auto;
}
</style>
使用 single-spa
安装 single-spa
pnpm install single-spa
改造 main.ts
import './assets/main.css';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import Antd from 'ant-design-vue';
import { registerApplication, start } from 'single-spa';
const apps = [];
// 注册子应用
for (let i = apps.length - 1; i >= 0; i--) {
registerApplication(apps[i]);
}
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.use(Antd);
app.mount('#app');
start();
集合 Vue2(webpack) 子项目
这里使用了现成的模板项目
引入 single-spa-vue
在子项目中引入 single-spa-vue
pnpm install single-spa-vue
改造 main.ts
import singleSpaVue from 'single-spa-vue';
const appOptions = {
el: '#microApp', // 这里对应基座项目中用于渲染子项目的id
router,
render: (h: any) => h(App),
};
const init = async () => {
await getConfig(() => {
// 单独运行
if (!(window as any).singleSpaNavigate) {
new Vue({
router,
render: (h) => h(App),
}).$mount('#app');
}
});
};
init();
// 基于基座应用,导出生命周期函数
const vueLifecycle = singleSpaVue({
Vue,
appOptions,
});
export function bootstrap(props: any) {
console.log('app1 bootstrap');
return vueLifecycle.bootstrap(() => {});
}
export function mount(props: any) {
console.log('app1 mount');
return vueLifecycle.mount(() => {});
}
export function unmount(props: any) {
console.log('app1 unmount');
return vueLifecycle.unmount(() => {});
}
改造 vue.config.js
通常情况下,通过 webpack 构建工具生成的 js 脚本,各个子应用对应的 js 脚本执行时是相互隔离的。
为了打破这种隔离,我们就需要对 output 配置项做改造,添加 libaray、libraryTarget 配置项,将子应用入口文件的返回值即生命周期方法暴露给 window,这样基座应用就可以从 window 中获取子应用的生命周期方法。
// vue.config.js
const { defineConfig } = require('@vue/cli-service');
const package = require('./package.json');
module.exports = defineConfig({
configureWebpack: {
publicPath: 'http://localhost:8081',
output: {
library: package.name,
libraryTarget: 'umd',
},
},
});
配置环境变量
配置环境变量,分为独立运行和微前端运行
在子项目根目录创建 .env 独立启动环境变量
NODE_ENV=development
VUE_APP_BASE_URL=/
NODE_ENV=development
VUE_APP_BASE_URL=/app1
NODE_ENV=development
VUE_APP_BASE_URL=/app1
改造路由
路由使用 history
模式,并使用环境变量中配置好的 BASE_URL
const createRouter = () =>
new VueRouter({
mode: 'history', // history模式
scrollBehavior: (to, from, savedPosition) => {
if (savedPosition) {
return savedPosition;
} else {
return { x: 0, y: 0 };
}
},
base: process.env.VUE_APP_BASE_URL, // 使用环境变量中配置的
routes: routers,
});
修改 package.json
修改 name,并配置微前端启动命令
{
"name": "app1",
"description": "single-spa子应用1(vue2)",
"scripts": {
"serve": "vue-cli-service serve",
"serve:micro": "vue-cli-service serve --mode micro",
"build": "vue-cli-service build --mode buildMicro"
}
}
运行项目
单独运行时
pnpm run serve
作为微前端子应用运行时
pnpm run serve:micro
两者没有太大区别,只不过会在 pathname
的开头加了/app1
前缀
注册子应用
// layout/src/main.ts
const createScript = (url: string) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
const firstScript = document.getElementsByTagName('script')[0];
firstScript.parentNode?.insertBefore(script, firstScript);
});
};
function loadVue2App(url: string, globalVar: string) {
// 支持远程加载子应用
return async () => {
await createScript(url + '/js/chunk-vendors.js');
await createScript(url + '/js/app.js');
// 这里的return很重要,需要从这个全局对象中拿到子应用暴露出来的生命周期函数
return window[globalVar as keyof typeof window];
};
}
const apps = [
{
name: 'app1',
app: loadVue2App('http://localhost:8081', 'app1'),
activeWhen: (location: any) => location.pathname.startsWith('/app1'),
customProps: {},
},
];
// 注册子应用
for (let i = apps.length - 1; i >= 0; i--) {
registerApplication(apps[i]);
}
测试效果
在 layout/App.vue 中增加入口
const items: ItemType[] = reactive([
getItem('子项目', '1', null, [
getItem('Vue', 'vue', null, [getItem('Vue2-1', 'app1')], 'group'),
getItem('React', 'react', null, []),
]),
]);
在子应用中添加组件、路由跳转等功能。
切换子应用
根据集合 Vue2(webpack) 子项目相同的步骤,创建子应用 app2,并将 app2 集合到基座应用
const apps = [
{
name: 'app1',
app: loadVue2App('http://localhost:8081', 'app1'),
activeWhen: (location: any) => location.pathname.startsWith('/app1'),
customProps: {},
},
{
name: 'app2',
app: loadVue2App('http://localhost:8082', 'app2'),
activeWhen: (location: any) => location.pathname.startsWith('/app2'),
customProps: {},
},
];
const items: ItemType[] = reactive([
getItem('子项目', '1', null, [
getItem(
'Vue',
'vue',
null,
[getItem('Vue2-1', 'app1'), getItem('Vue2-2', 'app2')],
'group'
),
getItem('React', 'react', null, [], 'group'),
]),
]);
子应用路由跳转报错
此时发现子应用中路由变更后切换子应用会出现报错的问题,详见single-spa 子应用路由跳转报错
集合 Vue3(vite+rollup) 子项目
搭建项目
在命令行中运行以下命令
pnpm create vue@latest
跟着步骤搭建项目
改造项目
删除 main.css 中会印象基座应用的样式
安装 single-spa-vue
pnpm install single-spa-vue
修改 main.ts
import './assets/main.css';
import { createApp, h } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import singleSpaVue from 'single-spa-vue';
// 单独运行
if (!(window as any).singleSpaNavigate) {
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');
}
// 基于基座应用,导出生命周期函数
const vueLifecycle = singleSpaVue({
createApp,
appOptions: {
el: '#microApp',
render() {
return h(App, {
name: this.name,
});
},
},
handleInstance(app) {
app.use(createPinia());
app.use(router);
},
});
export const bootstrap = vueLifecycle.bootstrap;
export const mount = vueLifecycle.mount;
export const unmount = vueLifecycle.unmount;
配置环境变量
配置环境变量,分为独立运行和微前端运行
在子项目根目录创建 .env 独立启动环境变量
NODE_ENV=development
VITE_APP_BASE_URL=/
NODE_ENV=development
VITE_APP_BASE_URL=/app3
NODE_ENV=development
VITE_APP_BASE_URL=/app3
将配置好的 BASE_URL 配置到路由中
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_APP_BASE_URL),
routes: [],
});
注册子应用
function loadVue3App(url: string) {
return async () => {
return import(`${url}/src/main.ts`);
};
}
const apps = [
...,
{
name: 'app3',
app: loadVue3App('http://localhost:5173'),
activeWhen: (location: any) => location.pathname.startsWith('/app3'),
customProps: {},
},
];
集合 React18(vite+ts) 子项目
搭建项目
使用 vite 搭建 React18 项目
pnpm create vite
选择 React 框架,并选择使用 TypeScript
警告
选择 TypeScript
,不要选择 TypeScript + SWC
,实测会出现问题导致子应用加载失败
构建完成后进入项目目录并安装依赖
cd ./app4
pnpm install
引入 single-spa-react
引入 single-spa-react
pnpm i single-spa-react
改造项目
先定义环境变量
NODE_ENV=development
VITE_APP_BASE_URL=/
NODE_ENV=development
VITE_APP_BASE_URL=/app4
NODE_ENV=development
VITE_APP_BASE_URL=/app4
这里引入 antd
作为 ui 框架,并使用 react-router-dom
进行路由管理
import { ConfigEnv, defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig((env: ConfigEnv) => {
const { mode } = env;
return {
plugins: [react()],
base: loadEnv(mode, process.cwd()).VITE_APP_BASE_URL,
server: {
port: 8084,
},
};
});
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import router from './router';
import { RouterProvider } from 'react-router-dom';
interface ISingle {
singleSpaNavigate: boolean;
}
type IWindow = ISingle & Window & typeof globalThis;
import singleSpaReact from 'single-spa-react';
console.log(router);
const rootComponent = () => {
return (
<React.StrictMode>
<ConfigProvider locale={zhCN}>
<RouterProvider router={router}></RouterProvider>
</ConfigProvider>
</React.StrictMode>
);
};
if (!(window as IWindow).singleSpaNavigate) {
ReactDOM.createRoot(document.getElementById('root')!).render(rootComponent());
}
const reactLifecycle = singleSpaReact({
React,
ReactDOMClient: ReactDOM,
rootComponent: rootComponent,
domElementGetter() {
return document.getElementById('microApp')!;
},
errorBoundary(err, errInfo, props) {
console.log(err, errInfo, props);
return <p>is Error</p>;
},
});
export const bootstrap = async (props: any) => {
return reactLifecycle.bootstrap(props);
};
export const mount = async (props: any) => {
console.log(props);
return reactLifecycle.mount(props);
};
export const unmount = async (props: any) => {
return reactLifecycle.unmount(props);
};
import { createBrowserRouter } from 'react-router-dom';
import Root from '../views/root';
import NextPage from '../views/next';
const router = createBrowserRouter(
[
{
path: '/',
element: <Root />,
},
{
path: '/nextPage',
element: <NextPage />,
},
],
{
basename: import.meta.env.VITE_APP_BASE_URL,
}
);
export default router;
注册子应用
function loadReactApp(url: string) {
return async () => {
return import(url + '/src/main.tsx')
}
}
const apps = [
...,
{
name: 'app4',
app: loadReactApp('http://localhost:8084/app4'),
activeWhen: (location: any) => location.pathname.startsWith('/app4'),
customProps: {}
}
]
这里选用的是 vite 构建的项目,会出现问题,详见single-spa 使用 vite-react 作为子项目开发环境报错