qiankun 微前端初体验

1,285 阅读8分钟

介绍

qiankun 基于 single-spa 实现; 什么是微前端?个人理解类似于后端的微服务;不会说某些代码层面的错误会导致整体宕机。这也对照上官网对 qiankun 特性的介绍;

  • 独立开发、独立部署:微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新;

  • 独立运行:开发时可以只运行某一个子应用;

  • 技术栈无关:任意前端技术都可接入;

  • 增量更新:与独立部署应该是差不多意思,可以单独更新某个子应用;

快速上手

开始之前先安利每日优鲜团队微前端实践;绝对的最佳实践。 下面示例中均使用 Vue 技术栈;

主应用

项目创建完成后,主应用安装qiankun;

npm install qiankun -s

在 main.js 中注册子应用;

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import { registerMicroApps, start } from 'qiankun';

Vue.config.productionTip = false;

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

/**
 * @param {*} app配置对象
 * @param {*} 生命周期钩子
 */
registerMicroApps([
  {
    name: 'vueApp',
    entry: '//localhost:3000',
    container: '#container',
    activeRule: '/vueApp'
  }
]);

start();

然后再 app.vue 中给子应用放一个容器;这个容器的 id 就是 registerMicroApps 中第一个参数配置对象中的 container;其他参数后面在介绍。

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
    <div id="container"></div>
  </div>
</template>

子应用

子应用不需要下载 qiankun 整体来说需要修改四个地方;

1,项目 src 新增 public-path.js 文件

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

2,在入口文件最顶部引入 public-path.js,修改并导出三个生命周期函数

import './public-path';
import Vue from 'vue';
import App from './App.vue';
import router from './router';

Vue.config.productionTip = false;

let instance = null;
function render(props = {}) {
  const { container } = props;

  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app');
}

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

// 必须把生命周期钩子导出
export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
  console.log('[vue] props from main framework', props);
  render(props);
}
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}

4,微应用建议使用 history 模式的路由,需要设置路由 base,值和它的 activeRule 是一样的

const router = new VueRouter({
  mode: 'history',
  // 独立运行时使用后面的那个,单独运行时使用第一个
  // base 要和 主应用中 registerMicroApps中的 activeRule 相同
  base: window.__POWERED_BY_QIANKUN__ ? '/vueApp/' : '/child/vue-app/',
  routes
});

export default router;

5,修改 webpack 打包,允许开发环境跨域和 umd 打包。新增 vue.config.js

const { name } = require('./package');

module.exports = {
  // 打包到指定的文件夹,部署的时候方便分辨当前是哪个应用
  outputDir: 'sub-vue',
  // 设置独立运行时的路由base
  publicPath: '/child/vue-app/',
  devServer: {
    port: 9099,
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`
    }
  }
};

最后一步,我们在修改主应用 registerMicroApps 配置中的 entry 然后再加一个路由跳转;

<!-- 主应用app.vue -->
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/appVue">goToSubVue</router-link>
    </div>
    <router-view/>
    <div id="container"></div>
  </div>
</template>
// 主应用 main.js
registerMicroApps([
  {
    name: 'vueApp',
    entry: '//localhost:9099/child/vue-app/',
    container: '#container',
    activeRule: '/vueApp'
  }
]);

预览

然后启动项目预览效果;记得先单独启动子应用进行测试不要在阴沟里翻了船!

micro-apps1.gif

生命周期

主应用生命周期

提到主应用的声明周期必然离不开 regsterMicroApps 这个方法;

registerMicroApps(apps, lifeCycles) ;

registerMicroApps(
  [
    {
      name: '',
      entry: '',
      container: '',
      activeRule: '',
      loader(loading) {},
      props: {}
    }
  ]
)
  • name :子应用的名称,保证各个微应用之前不能重复
  • entry:子应用的入口,和子应用的启动服务保持一致,比如子应用启动后 192.168.10.1:8088/child/app-vue/ 那么 entry 则可以使 //localhost:8088/child/app-vue/ 或者 /child/app-vue/
  • container:对应子应用的需要载入哪个DOM容器中,父应用中去配置。
  • activeRule:微应用的激活入口,要与子应用的路由 window.__POWERED_BY_QIANKUN__ 为 true 是配置一致; 比如子应用 /vueApp/ 那么主应用的activeRule/vueApp
  • loader:可选参数 子应用的 loading 效果。
  • props :此值可以在子应用的 mount 钩子中拿到,注意:props在子应用中获取到的并不是一个空对象,注意键名冲突
registerMicroApps(
	[{}],
  {
    beforeLoad(app) {},
    beforeMount(app) {},
    afterMount(app) {},
    beforeUnmount(app) {},
    afterUnmount: [app => console.log(app)]
  }
)

以上声明周期钩子中 app 均为可选参数,获取当前被激活的子应用信息;当加载子应用时触发 beforeLoadbeforeMountafterMount 三个钩子,当离开子应用时触发 beforeUnmountafterUnmount

子应用声明周期

  • bootstrap:只有在子应用初始化的时候才会触发,其他时候不会触发;
  • mount:接收一个 props 即父应用中传递的 props ,每次进入子应用都会调用此钩子;ps:如果是 react 或者 vue 应用可以在此钩子中去挂载应用;
  • unmount:接收一个 props 即父应用中传递的 props,应用每次离开都会触发这个钩子;ps:可以做一些清空数据恢复状态的操作;
  • update:接收一个 props 即父应用中传递的 props,仅使用 loadMicroApp 方式加载微应用时生效。此方法是可选的;

注意:子应用的钩子是独立的 Promise 函数,需要单独调用,下面以 Vue 项目中 main.js 为例;

C1FB73534A47F742A15C3B68C9B2E243.jpg

父子应用通信

想要深入了解 qiankun 的通信推荐 qiankun父子通信

根据 qiankun 官方说的方式;有两种通信方式;

1,基于 initGlobalState 方法通信;使用此种方式我们先对主子应用稍微改装下。使用此种方式,我个人理解与 vue 组件之间通信类似;主应用负责维护数据,子应用只做消费;

initGlobalState:该方法接收一个 state,返回三个通信方法。

  • onGlobalStateChange(cb, fireImmediately):当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback

  • setGlobalState(state):按一级属性设置全局状态,微应用中只能修改已存在的一级属性

  • offGlobalStateChange:移除当前应用的状态监听,微应用 umount 时会默认调用

// 主应用:shared/actions.js
import { initGlobalState } from 'qiankun';

const state = {};
const actions = initGlobalState(state);

export default actions;
// 子应用:shared/actions.js
function emptyAction () {
  // 提示当前使用的是空 Action
  console.warn('Current execute action is empty!');
}

class Actions {
  // 默认值空 action
  actions = {
    onGlobalStateChange: emptyAction,
    setGlobalState: emptyAction,
  };
  
  /**
   * 设置 actions
   */
  setActions(actions) {
    this.actions = actions;
  }
  
  /**
   * 映射
   */
  onGlobalStateChange(...args) {
    return this.actions.onGlobalStateChange(...args);
  }

  /**
   * 映射
   */
  setGlobalState(...args) {
    return this.actions.setGlobalState(...args);
  }
};

const actions = new Actions();
export default actions;

完成以上配置,需要在子应用的 mount 钩子中去设置这个 actions;

import actions from '../shared/actions';
// ......
export async function mount (props) {
  console.log('[vue] props from main framework', props);
  actions.setActions(props);
  render(props);
}

做完上面的操作,我们来写个示例

// 主应用Home.vue
<template>
	<div>
    <fieldset>
      <legend>formData</legend>
      username: <input type="text" v-model="formData.username" /><br />
      password: <input type="text" v-model="formData.password" /> <br />
      <button @click="submit">submit</button>
    </fieldset>
  </div>
</template>

<script>
import actions from '../../shared/actions';
  
export default {
  data() {
    return {
      formData: {
        username: '',
      	password: ''
      }
    }
  },
  methods: {
    submit() {
      actions.setGlobalState(this.formData)
    }
  }
}
</script>
// 子应用 Home.vue
<template>
	<div>{{ form }}</div>
</template>
<script>
export default {
  data() {
    return {
      form: {}
    };
  },
  mounted() {
    // 由于我们是在父应用的Home中跳转到子应用;
    // 实际上是做了 / 跳转到 /vueApp 的操作,但是还要能够拿到 / 中设置的内容;虽然我们设置成功了,但是子应用挂载的时候已经没有change事件了;
    // 所以加上第二个参数 true,类似于 watch 中的 immediate 。
    actions.onGlobalStateChange((state, prev) => {
      this.form = state;
    }, true);
  }
}
</script>

QQ20220117-170529-HD2.gif

看到上面还是有点问题的如果我们想要从 /vueApp 中返回 / 时 输入框内仍然存在我们输入的值;这时需要在 主应用的 Home.vue 再次监听一下 change 事件

export default {
  // ....
  mounted() {
    // 这里同样要加上 true
    actions.onGlobalStateChange((state, prev) => {
      this.formData = state;
    }, true)
  }
}

在尝试这个通信的时候发现 qiankun 还有一个机制,在一个应用中存在多个 change 事件,只会最后一个生效,防止内存爆炸;

总结: 实际操作下来;感觉不足的地方,当然大概率是我菜

  • 更新 state 的方式;也就是说通过 setGlobalState 但是官网对于这个方法的介绍是更新一级属性;按照我的示例更新,岂不是每次都等同于覆盖了原有的 state?在假设如果我们有 2个以上的子应用,state 应该如何维护?希望评论区也有人解答这个问题;
  • 假设上面的问题成立;我现在有两个子应用,一个是需要更新 state 中 userInfo 的信息,一个是需要更新 state 中 systemInfo 的信息;(子给父传值) 此时怎么保证能够更新数据,并且不用全量修改整个 state?

2,基于 registerMicroApps 中 app 配置对象中的 props 进行通信;参考上面的通信链接;不写了。不过也可能是比较优解的方案。

部署

以阿里云为例 先安装 nginx 可以通过 yum 安装

yum install nginx

nginx 安装成功后 配置文件在 /etc/nginx/nginx.conf; 静态资源在/usr/local/html

官网提供了两种部署方式;

1,微应用放在一个特殊的文件夹下(不会和微应用重名)这种也是比较推荐的;

└── html/                     # 根文件夹
    |
    ├── child/                # 存放所有微应用的文件夹
    |   ├── vue-app/          # 存放微应用 vue-app 的文件夹 打包时通过 outputDir 指定
    ├── index.html            # 主应用的index.html
    ├── css/                  # 主应用的css文件夹
    ├── js/                   # 主应用的js文件夹
server {
    listen       80;
    listen       [::]:80;
    server_name  _;

    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods *;
    add_header Access-Control-Allow-Headers *;

                            # 这里会去dist中去寻找主应用的静态资源
    location / {
        root     /usr/local/html/dist;
        index    index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    # 这里其实访问的是子应用的 publickPath,同时这个path要和 主应用的 registerMicroApps 中配置对象中的 entry 一致
    location /child/appVue {
        root     /user/local/html/dist;
        index    index.html index.htm;
        try_files $uri $uri/ /child/app-vue/index.html;
    }

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;
}

此时 主应用中 registerMicroApps 的配置可以是下面这样;

但是这里有个不解的地方;如果我本地启动启动所有服务 entry 如果没有 //localhost:9099 就会运行不起来,但是发布到生产环境就可以;

registerMicroApps([
  {
    name: 'vueApp',
    // entry: '//localhost:9099/child/vue-app/',
    entry: '/child/vue-app/',
    container: '#container',
    activeRule: '/vueApp'
  }
]);

2,就是直接放到二级目录,不用在 dist 中创建 child 文件夹了,操作基本一致,只需要简单改下 linux 配置即可

└── html/                     # 根文件夹
    |
    ├── vue-app/              # 存放微应用 vue-app 的文件夹
    ├── index.html            # 主应用的index.html
    ├── css/                  # 主应用的css文件夹
    ├── js/                   # 主应用的js文件夹
 # 这里其实访问的是子应用的 publickPath,同时这个path要和 主应用的 registerMicroApps 中配置对象中的 entry 一致
 location /child/appVue {
    root     /user/local/html/dist;
    index    index.html index.htm;
    # 这里把 /child去掉直接去 app-vue中查找入口文件
    try_files $uri $uri/ /app-vue/index.html;
 }

3,还有一种情况是子应用和主应用不在一个服务器,这个真心没法测试,兜里没钱😂;不同服务器部署微应用