跳到主要内容

vite-vue3-JSEntry-System-Parcel

一、认识


1.1 SystemJs

SystemJs是一个通用的模块加载器,有属于自己的模块化规范。他能在浏览器和node环境上动态加载模块,微前端的核心就是加载子应用,因此将子应用打包成模块,在浏览器中通过SystemJs来加载模块。SystemJS可兼容到IE11,但是它对于插件版本要求非常严格,而且变化非常大,兼容性也不是特别好,使用体验也不是很好,所以目前实践中用的非常少。它同样支持import映射,但是它的语法稍有不同。

二、实现


2.1 index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Main-App-Vue3</title>
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js",
"react-router-dom":"https://cdn.jsdelivr.net/npm/react-router-dom@5.3.3/umd/react-router-dom.min.js",
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.8.2/lib/system/single-spa.min.js",
"single-spa-react": "https://cdn.jsdelivr.net/npm/single-spa-react@4.6.1/lib/system/single-spa-react.js",
"single-spa-vue": "https://cdn.jsdelivr.net/npm/single-spa-vue@2.5.1/dist/system/single-spa-vue.js",
"micro-app-vue3": "https://bolawen.github.io/MicroFrontEnd/Single-Spa-BySystem/micro-app-vue3/output/micro-app-vue3.system.4646c519.js",
"micro-app-react":"https://bolawen.github.io/MicroFrontEnd/Single-Spa-BySystem/micro-app-react/output/micro-app-react.092b73cb.js"
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@1.16.0/dist/import-map-overrides.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.4.0/dist/system.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.4.0/dist/extras/amd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.4.0/dist/extras/named-exports.js"></script>
</head>
<body>
<div id="main-app-vue3"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2.2 src/main.ts

import { registerApplication, start } from "single-spa";
import * as app from "./single-spa-config";
import packageInfo from "../package.json";
registerApplication({
name: packageInfo.name,
app: app,
activeWhen: () => true,
customProps: {
base: "/",
mode: "history",
},
});
start({
urlRerouteOnly: true,
});

2.3 src/single-spa-config.ts

import App from "./App.vue";
import { h, createApp } from "vue";
import createRouter from "./router";
import { AppProps } from "single-spa";
import singleSpaVue from "single-spa-vue";

type PropsType = AppProps & { base: string; mode: "hash" | "history" };

const vueLifecycles = singleSpaVue({
createApp,
appOptions: {
render(props: PropsType) {
return h(App, {
...props,
});
},
},
handleInstance(app, props: PropsType) {
const { base, mode } = props;
app.use(createRouter(base, mode));
},
});

export const { bootstrap, mount, unmount, update } = vueLifecycles;

2.4 src/App.vue

<template>
<div>Main-App-Vue3 基座Demo</div>
<div>
<router-link to="/">首页</router-link>
<router-link to="/content">内容</router-link>
</div>
<router-view></router-view>
</template>

<script lang="ts">
import { AppProps } from "single-spa";
import { defineComponent, PropType, ref } from "vue";

export default defineComponent({
name: "App",
props: {
base: String,
mode: String,
name: String,
mountParcel: Function as PropType<AppProps["mountParcel"]>,
singleSpa: Object as PropType<AppProps["singleSpa"]>,
},
setup(props) {
console.log("Main-App-Vue3 Props", props);
},
});
</script>

<style scoped></style>

2.5 src/router.ts

import {
Router,
createRouter,
RouteRecordRaw,
createWebHistory,
createWebHashHistory,
} from "vue-router";
import Home from "./Home.vue";
import Content from "./Content.vue";

const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/content/:microApp*",
name: "Content",
component: Content,
},
];

export default function create(base?: string, mode?: string): Router {
return createRouter({
history:
mode === "history" ? createWebHistory(base) : createWebHashHistory(base),
routes,
});
}

2.6 src/Home.vue

<template>
<h3>基座首页</h3>
</template>

<script lang="ts" setup></script>

<style scoped></style>

2.7 src/Content.vue

<template>
<button @click="onClick('micro-app-vue3')">切换 micro-app-vue3 微应用</button>
<button @click="onClick('micro-app-react')">切换 micro-app-react 微应用</button>
<component :is="components.get(compName)"></component>
</template>

<script lang="ts" setup>
import loadMicro from "./loadMicro";
import { defineAsyncComponent, ref } from "vue";

const components = ref(new Map<string, any>());
components.value.set(
"micro-app-vue3",
defineAsyncComponent(loadMicro("micro-app-vue3"))
);
components.value.set(
"micro-app-react",
defineAsyncComponent(loadMicro("micro-app-react"))
);

const compName = ref();

function onClick(microName) {
compName.value = microName;
}
</script>

<style scoped></style>

2.8 src/loadMicro.ts

import { h, ref } from "vue";
import Parcel from "./parcel";

export default function (path: string) {
return async function () {
const parcelConfig = await window.System.import(path);
return {
setup() {
let parcelProps = ref({
base: '/content',
mode: "history",
});
return () =>
h(Parcel, {
path,
config: parcelConfig,
parcelProps: parcelProps.value,
});
},
};
};
}

2.9 src/parcel.ts

import { mountRootParcel } from "single-spa";
import { defineComponent, h, onMounted, onUnmounted, ref, watch } from "vue";

export default defineComponent({
name: "Parcel",
props: {
config: [Object, Promise],
path: String,
wrapWith: String,
wrapClass: String,
wrapStyle: Object,
parcelProps: Object,
},
setup(props) {
const { config, parcelProps, wrapClass, wrapStyle, wrapWith, path } = props;
const el = ref<HTMLElement | null>(null);
const parcel = ref<any>(null);
onMounted(() => {
parcel.value = mountRootParcel(config, {
...parcelProps,
domElement: el.value,
});
parcel.value.mountPromise.then(() => {
console.log(`${path}-挂载完成`);
});
});

onUnmounted(() => {
if (parcel.value) {
parcel.value.unmount();
parcel.value.unmountPromise.then(() => {
console.log(`${path}-卸载完成`);
});
}
});

watch(
() => props.parcelProps,
(value) => {
if (typeof parcel.value.update === "function") {
parcel.value
.update({
...value,
domElement: el.value,
})
.then(() => {
console.log("parcel updated");
});
}
}
);

return () =>
h("div", {
ref: el,
class: wrapClass,
style: wrapStyle,
});
},
});

2.10 package.json

{
"name": "main-app-vue3",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"single-spa": "^5.9.4",
"single-spa-vue": "^2.5.1",
"vue": "^3.2.37",
"vue-router": "^4.0.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.0",
"typescript": "^4.6.4",
"vite": "^3.0.0",
"vue-tsc": "^0.38.4"
}
}

2.11 vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import packageInfo from "./package.json";

export default ({ mode }) => {
return defineConfig({
plugins: [vue()],
base:
mode === "production"
? `https://bolawen.github.io/MicroFrontEnd/Single-Spa-BySystem/${packageInfo.name}/`
: "/",
server: {
port: 3000,
},
build: {
assetsDir: "",
outDir: "output",
cssCodeSplit: false,
rollupOptions: {
input: "src/single-spa-config.ts",
preserveEntrySignatures: "exports-only",
external: ["axios", "lodash", "single-spa", "single-spa-vue", "moment"],
output: {
format: "system",
entryFileNames: `${packageInfo.name}.[format].[hash].js`,
},
},
manifest: true,
},
});
};