【老奶奶都能学得会系列】qiankun demo助你快速入门微前端

时间:2021-1-8 作者:admin

微前端的介绍,大家已经看过很多了,这里就不多做介绍了,直接上手demo,快速入门,连老奶奶都学得会咯!(文末有项目代码)~

初始化项目

  1. 初始化主应用

初始化项目(安装的时候勾选上vue routervuex

vue create main-app

安装UI框架element-ui、微前端框架qiankun

npm i element-ui qiankun --save
  1. 初始化子应用

初始化项目(安装的时候勾选上vue routervuex

vue create main-app

安装UI框架element-ui

npm i element-ui --save

基础版

编写主应用代码

  1. /src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'vue', // 子应用名称
    entry: '//localhost:10000', // 子应用入口
    container: '#vue', // 子应用在主应用中显示的位置
    activeRule: '/vue', // 子应用激活的路由规则
  }
]);
start();

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

代码详解

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

引入element-ui

import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
  {
    name: 'vue', // 子应用名称
    entry: '//localhost:10000', // 子应用入口
    container: '#vue', // 子应用在主应用中显示的位置
    activeRule: '/vue', // 子应用激活的路由规则
  }
]);
start();

微应用有两种加载方式,一种是基于路由的加载方式,另外一种是基于手动的加载方式,这里我们使用第一种。

使用registerMicroApps进行配置。registerMicroApps(apps, lifeCycles?)接收两个参数,第一个必填项,填入微应用的注册信息。第二个为生命周期钩子,选填,可填入的项目有beforeLoadbeforeMountafterMountbeforeUnmountafterUnmount,这里为了降低demo的理解难度,先不填入了,只使用第一个参数。

使用start开启qiankun

  1. /src/App.vue
<template>
  <div>
    <el-menu :router="true" mode="horizontal" default-active="/">
      <el-menu-item index="/">Home</el-menu-item>
      <el-menu-item index="/about">About</el-menu-item>
      <el-menu-item index="/vue">Vue</el-menu-item>
    </el-menu>
    <router-view></router-view>
    <div id="vue"></div>
  </div>
</template>

代码详解

<el-menu :router="true" mode="horizontal" default-active="/">
      <el-menu-item index="/">Home</el-menu-item>
      <el-menu-item index="/about">About</el-menu-item>
      <el-menu-item index="/vue">Vue</el-menu-item>
</el-menu>  

element-ui提供的菜单栏

<router-view></router-view>

作为主应用本身的路由组件容器

<div id="vue"></div>

作为子应用的容器,跟main.js里面registerMicroApps注册的container要一致。

  1. /src/views/Home.vue
<template>
  <div class="home">
    <h1>主应用 Home</h1>
  </div>
</template>
  1. /src/views/About.vue
<template>
  <div class="about">
    <h1>主应用 About</h1>
  </div>
</template>

编写子应用代码

  1. /src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import router from './router'

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

Vue.config.productionTip = false

let instance;

function render() {
  // 加载vue实例
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#app')
}

// 独立运行微应用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

// 被主应用使用时
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}


export async function bootstrap() {}

export async function mount(props) {
  render(props)
}

export async function unmount(props) {
  instance.$destroy()
  instance = null
}

代码详解

let instance;

function render() {
  // 加载vue实例
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#app')
}

这里要使用一个render函数将原有的实例Vue的过程包裹起来是有两个原因,一个是我们需要在主应用需要渲染子应用的时候再去渲染子应用,另外是我们也需要支持可以独立运行子应用,因此需要让渲染这个过程变得具备可控性,需要渲染的时候再去调用render函数进行渲染

// 独立运行微应用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

可以window.__POWERED_BY_QIANKUN__通过这个全局变量来区分当前是否运行在qiankun的主应用的上下文中,如果该变量不存在,则证明当前子应用应当独立运行,所以调用render函数进行渲染

// 被主应用使用时
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

我们的主应用使用的端口是8080,而我们的子应用使用的是10000,如果不设置这个,我们对子应用路由的请求会以http://localhost:8080开头,而不是以正确的http://localhost:10000开头。

runtime publicPath 主要解决的是微应用动态载入的 脚本、样式、图片 等地址不正确的问题。

export async function bootstrap() {}

export async function mount() {
  render()
}

export async function unmount() {
  instance.$destroy()
}

根据规定,微应用需要在自己的入口js文件导出bootstrapmountunmount三个生命周期钩子,以供主应用在适当的时机调用。

  • bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用mount钩子,不会再重复触发 bootstrap。通常我们可以在这里做一些全局变量的初始化,比如不会在unmount阶段被销毁的应用级别的缓存等。
  • 应用每次进入都会调用mount方法,通常我们在这里触发应用的渲染方法
  • 应用每次切出/卸载会调用的方法,通常在这里我们会卸载微应用的应用实例

因此,我们在mount里面进行子应用的渲染,在unmount里面进行子应用的卸载。

  1. /src/App.vue
<template>
  <div class="child-app">
    <el-menu :router="true" default-active="/" class="child-menu">
      <el-menu-item index="/">Home</el-menu-item>
      <el-menu-item index="about">About</el-menu-item>
    </el-menu>
    <router-view />
  </div>
</template>

<style scoped>
.child-app {
  width: 400px;
  display: flex;
  border: 1px solid black;
  margin-top: 20px;
  padding: 10px;
}
.child-menu {
  margin-right: 20px;
}
</style>
  1. /src/view/Home.vue
<template>
  <div class="home">
    <h2>子应用 Home</h2>
  </div>
</template>
  1. /src/views/About.vue
<template>
  <div class="about">
    <h2>子应用 About</h2>
  </div>
</template>
  1. /src/router/index.js
//...
const router = new VueRouter({
  mode: 'history',
  base: window.__POWERED_BY_QIANKUN__ ? '/vue' : process.env.BASE_URL,
  routes
})
//...

作为子应用运行时,子应用的路由生效规则应该和跟主应用main.js里面registerMicroApps注册的activeRule要一致,所以设置base'/vue'

  1. /vue.config.js
module.exports = {
    devServer: {
        port: 10000,
        headers: {
            'Access-Control-Allow-Origin': '*' // 主应用获取子应用时跨域响应头
        }
    },
    configureWebpack: {
        output: {
            library: `vue`,
            libraryTarget: 'umd',
        }
    }
}

由于是8080端口到10000端口的访问,我们需要配置跨域允许,另外,为了让主应用能正确识别微应用暴露出来的一些信息,还需要多加些webpack的打包配置。

开跑

至此,主应用和子应用准备完毕,开跑!

  1. 测试子应用

打开http://localhost:10000

  1. 测试主应用

打开http://localhost:8080

如果你也能看到如下的效果,证明主应用、子应用都完美运行了

通信版

从上面的效果来看,我们已经实现微前端的基础使用了,成功的将子应用嵌入到主应用中去,但是,我们在项目中,经常会涉及到一些需要主应用和子应用通信的场景,由于两个都是vue项目,所以我们使用vuex来实现通信。

编写主应用

  1. /src/main.js
// ...
registerMicroApps([
  {
    name: 'vue', // app name registered
    entry: '//localhost:10000',
    container: '#vue',
    activeRule: '/vue',
    // 多加了下面这段
    props: {
      initState: store.state  
    }
  }
]);
// ...

把主应用的state,使用props,传递给子应用,子应用可在导出的mount函数里面接收

  1. /src/store/index.js
// ...
mutations: {
  addNum(state, n) {
    state.num += n
  }
}
// ...
  1. /src/App.vue
<template>
  <div>
    <el-menu :router="true" mode="horizontal" default-active="/">
      <el-menu-item index="/">Home</el-menu-item>
      <el-menu-item index="/about">About</el-menu-item>
      <el-menu-item index="/vue">Vue</el-menu-item>
    </el-menu>
    <h1>主应用的state.num:{{$store.state.num}}</h1>
    <button @click="addNum(10)">主应用增加10</button>
    <router-view></router-view>
    <div id="vue"></div>
  </div>
</template>

<script>
export default {
  methods: {
    addNum(n) {
      this.$store.commit('addNum', 10)
    }
  }
}
</script>

编写子应用

  1. /src/main.js
// ...
function render(props = {}) {
  if (!store.hasModule('global')) {
    // 当独立运行微应用时,state需要初始化赋值,否则会报错
    const initState = props.initState || {
      num: 0
    }
    // 将父应用的数据存储到子应用中,命名空间固定为global
    const globalModule = {
      namespaced: true,
      state: initState,
      mutations: {
        addNum(state, n) {
          state.num += n
        }
      },
    };
    // 注册一个动态模块
    store.registerModule('global', globalModule);
  }
  // 加载vue实例
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#app')
}
// ...
export async function mount(props) {
  render(props)
}
// ...
  • mount接收主应用传递过来的数据,将其传递给render函数
  • render函数先判断子应用store里面有没有注册过global模块,如果没有的话,使用props接收主应用传递过来的state,并且将其作为global模块的state数据来源,动态注册。之后,我们使用global模块,实际上就是使用父传递过来的数据,由此实现数据通信。
  1. /src/views/Home.vue
<template>
  <div class="home">
    <h2>子应用 Home</h2>
    <h2>主应用的state.num:{{$store.state.global.num}}</h2>
    <button @click="addNum(10)">主应用增加20</button>
  </div>
</template>

<script>
export default {
  methods: {
    addNum() {
      this.$store.commit('global/addNum', 20)
    }
  }
}
</script>

开跑

至此,主应用和子应用准备完毕,开跑!

  1. 测试子应用

打开http://localhost:10000

可以看到,此时子应用中的按钮也是可以正常点击的,这是因为在render函数中,我们给了其初始值的缘故

const initState = props.initState || {
  num: 0
}
  1. 测试主应用

打开http://localhost:8080

可以看到,无论是点击主应用里面的按钮,还是子应用里面的按钮,数据都能做到同步响应式的更新,这证明我们的数据通信已经成功实现!

项目代码

github.com/xiezijie439…
如果对你有帮助的话,麻烦给我一颗小星星~

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。