create-single-spa
使用single-spa的cli工具创建项目,create-single-spa文档
npm install --global create-single-spa
yarn global add create-single-spa
然后运行以下命令:
create-single-spa
或者,您可以在没有全局安装的情况下使用 create-single-spa:
npm init single-spa
npx create-single-spa
yarn create single-spa
这将打开一个 CLI 提示,询问您要创建或更新哪种项目。
Vue子应用
使用create-single-spa创建一个Vue子应用项目,如果第一次创建没有成功,可以再按照第一次创建的步骤再来一遍,cli提示你如何处理已存在内容时选择merge就好了。
启动项目查看是否可以运行:
npm install
npm run serve
pnpm install
pnpm run serve
Angular主应用
主应用内使用Systemjs加载Vuejs文件时,名称为Vue子项目的package.json > name
例如下面的@vue-child/vue-child,就是我创建的Vue子应用内package.json配置中的name的值
imports: {
child1: 'http://localhost:4201/main.js',
child2: 'http://localhost:4201/main.js',
'@vue-child/vue-child': 'http://localhost:8080/js/app.js',
},
index.html中引入Vuejs
<script src="/assets/vue/vue.global.js"></script>
Systemjsmap增加Vue项目js地址
'@vue-child/vue-child': 'http://localhost:8080/js/app.js',
我们需要一个Vue组件来加载Vue项目然后渲染到页面中,但是在Angular中并不能直接书写.vue组件,我们可以使用引入的全局Vue里面的Api来实现Vue组件的编写及渲染
创建一个VueParcel组件,vue-parcel.component.ts,参考single-spa-vue
const lessThanVue3 = !Vue.version || /^[012]\..+/.test(Vue.version);
declare var Vue: any;
export const VueParcel = {
props: {
config: [Object, Promise],
wrapWith: String,
wrapClass: String,
wrapStyle: Object,
mountParcel: Function,
parcelProps: Object,
},
render(h) {
h = typeof h === 'function' ? h : Vue.h || (Vue.default && Vue.default.h);
const containerTagName = this.wrapWith || 'div';
const props = { ref: 'container' };
if (this.wrapClass) {
props.class = this.wrapClass;
}
if (this.wrapStyle) {
props.style = this.wrapStyle;
}
return h(containerTagName, props);
},
data() {
return {
hasError: false,
nextThingToDo: null,
};
},
methods: {
addThingToDo(action, thing) {
if (this.hasError && action !== 'unmount') {
return;
}
this.nextThingToDo = (this.nextThingToDo || Promise.resolve())
.then((...args) => {
if (this.unmounted && action !== 'unmount') {
return;
}
return thing.apply(this, args);
})
.catch((err) => {
this.nextThingToDo = Promise.resolve();
this.hasError = true;
if (err && err.message) {
err.message = `During '${action}', parcel threw an error: ${err.message}`;
}
this.$emit('parcelError', err);
throw err;
});
},
singleSpaMount() {
this.parcel = this.mountParcel(this.config, this.getParcelProps());
return this.parcel.mountPromise.then(() => {
this.$emit('parcelMounted');
});
},
singleSpaUnmount() {
if (this.parcel) {
return this.parcel.unmount();
}
},
singleSpaUpdate() {
if (this.parcel && this.parcel.update) {
return this.parcel.update(this.getParcelProps()).then(() => {
this.$emit('parcelUpdated');
});
}
},
getParcelProps() {
return {
domElement: this.$refs.container,
...(this.parcelProps || {}),
};
},
},
mounted() {
if (!this.config) {
throw Error(`single-spa-vue: <vue-parcel> component requires a config prop.`);
}
if (!this.mountParcel) {
throw Error(`single-spa-vue: <vue-parcel> component requires a mountParcel prop`);
}
if (this.config) {
this.addThingToDo('mount', this.singleSpaMount);
}
},
[lessThanVue3 ? 'destroyed' : 'unmounted']() {
this.addThingToDo('unmount', this.singleSpaUnmount);
},
watch: {
parcelProps: {
handler() {
this.addThingToDo('update', this.singleSpaUpdate);
},
},
},
};
再创建一个公共的子应用宿主组件,参考single-spa
spa-vue-host.component.ts
import { AfterViewInit, ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { VueParcel } from './vue-parcel.component';
import { mountRootParcel } from 'single-spa';
import { LwFunctionService } from '@leafpoda/hiplanet/services';
declare var Vue: any;
@Component({
selector: 'app-spa-main-vue-host',
template: `<div [id]="andomId"></div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VueSpaHostComponent implements AfterViewInit {
constructor(private router: ActivatedRoute, private fns: LwFunctionService) {}
appName = '';
andomId = 'vueHost' + this.fns.randomNum(1, 10000);
ngAfterViewInit(): void {
this.router.data.subscribe((data) => {
this.appName = data.app;
this.createVueApp();
});
}
createVueApp() {
const appName = this.appName;
const vueApp = Vue.createApp({
template: `
<vue-parcel
v-on:parcelMounted="parcelMounted()"
v-on:parcelUpdated="parcelUpdated()"
:config="parcelConfig"
:mountParcel="mountParcel"
:wrapWith="wrapWith"
:wrapClass="wrapClass"
:wrapStyle="wrapStyle"
:parcelProps="getParcelProps()"
>
</vue-parcel>
`,
components: {
VueParcel,
},
data() {
return {
parcelConfig: System.import(appName).then((ns: any) => {
return ns;
}),
mountParcel: mountRootParcel,
wrapWith: 'div',
wrapClass: '',
wrapStyle: {},
};
},
methods: {
getParcelProps() {
return {
text: `Hello world`,
};
},
parcelMounted() {
console.log('parcel mounted');
},
parcelUpdated() {
console.log('parcel updated');
},
},
});
vueApp.mount('#' + this.andomId);
}
}
将宿主组件放入SharedModule中
接下来我们创建一个视图组件,路由中传入需要加载的Vue子应用名称
vue3-child.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from 'src/app/shared/shared.module';
import { Routes, RouterModule } from '@angular/router';
import { Vue3ChildComponent } from './vue3-child.component';
const routes: Routes = [
{
path: '',
component: Vue3ChildComponent,
data: {
app: '@vue-child/vue-child',
},
},
];
@NgModule({
declarations: [Vue3ChildComponent],
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
})
export class Vue3ChildModule {}
html中使用app-spa-main-vue-host宿主组件进行渲染
vue3-child.component.html
<nz-layout>
<nz-header>
<div class="logo"></div>
<app-spa-main-header-menu></app-spa-main-header-menu>
</nz-header>
<nz-content>
<div class="inner-content">
<app-spa-main-vue-host></app-spa-main-vue-host>
</div>
</nz-content>
</nz-layout>
vue3-child.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-spa-main-vue3-child',
templateUrl: './vue3-child.component.html',
styleUrls: ['./vue3-child.component.less'],
})
export class Vue3ChildComponent {}
路由配置中加入
{
path: 'vue3child',
loadChildren: () =>
import('./features/vue3-child/vue3-child.module').then((m) => m.Vue3ChildModule),
},
到这里我们就实现了在Angular主应用中接入Vue子应用
打包后的Vue接入
我们build后的js文件都是带有哈希值的,那么我们在主应用中引入的js文件名就需要是动态的名称,这里有多种解决方案,没有固定方法,视情况而定即可。
|