微前端single-spa:在Angular主应用中接入Vue子应用

create-single-spa

使用single-spa的cli工具创建项目,create-single-spa文档

npm install --global create-single-spa

# or
yarn global add create-single-spa

然后运行以下命令:

create-single-spa

或者,您可以在没有全局安装的情况下使用 create-single-spa:

npm init single-spa

# or
npx create-single-spa

# or
yarn create single-spa

这将打开一个 CLI 提示,询问您要创建或更新哪种项目。

Vue子应用

使用create-single-spa创建一个Vue子应用项目,如果第一次创建没有成功,可以再按照第一次创建的步骤再来一遍,cli提示你如何处理已存在内容时选择merge就好了

启动项目查看是否可以运行:

npm install
npm run serve

# or
pnpm install
pnpm run serve

Angular主应用

主应用内使用Systemjs加载Vuejs文件时,名称为Vue子项目的package.json > name

例如下面的@vue-child/vue-child,就是我创建的Vue子应用内package.json配置中的name的值

imports: {
  // child1: 'http://127.0.0.1:5500/mainjsFileName',
  child1: 'http://localhost:4201/main.js',
  // child2: 'http://127.0.0.1:5500/main.03c6ec5566be88088f4a.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

// @ts-nocheck
const lessThanVue3 = !Vue.version || /^[012]\..+/.test(Vue.version);

// 添加 Vue 的声明
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';

// 添加 Vue 的声明
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 (object, required)
    
            The parcelConfig is an object, or a promise that resolves with a parcel config object.
            The object can originate from within the current project, or from a different
            microfrontend via cross microfrontend imports. It can represent a Vue component,
            or a React / Angular component.
            https://single-spa.js.org/docs/recommended-setup#cross-microfrontend-imports
    
            Vanilla js object:
            parcelConfig: {
              async mount(props) {},
              async unmount(props) {}
            }
    
            // React component
            parcelConfig: singleSpaReact({...})
    
            // cross microfrontend import is shown below
          */
          // @ts-ignore
          parcelConfig: System.import(appName).then((ns: any) => {
            return ns;
          }),

          /*
            mountParcel (function, required)
    
            The mountParcel function can be either the current Vue application's mountParcel prop or
            the globally available mountRootParcel function. More info at
            http://localhost:3000/docs/parcels-api#mountparcel
          */
          mountParcel: mountRootParcel,

          /*
            wrapWith (string, optional)
    
            The wrapWith string determines what kind of dom element will be provided to the parcel.
            Defaults to 'div'
          */
          wrapWith: 'div',

          /*
            wrapClass (string, optional)
    
            The wrapClass string is applied to as the CSS class for the dom element that is provided to the parcel
          */
          wrapClass: '',

          /*
            wrapStyle (object, optional)
    
            The wrapStyle object is applied to the dom element container for the parcel as CSS styles
          */
          wrapStyle: {},
        };
      },
      methods: {
        // These are the props passed into the parcel
        getParcelProps() {
          return {
            text: `Hello world`,
          };
        },
        // Parcels mount asynchronously, so this will be called once the parcel finishes mounting
        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文件名就需要是动态的名称,这里有多种解决方案,没有固定方法,视情况而定即可。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值