微前端应用实践


2021-08-27 前端

当一个应用足够复杂时,前端页面模块多,会导致前端运行及打包时间越来越长,而我们有时候往往只是修改了几个文件而已;

如果解决这个问题,微前端就是一种利用微件拆分来达到工程拆分的治理方案;微前端的概念应该还是借鉴了后端微服务的概念,将复杂繁多的单个单体服务按场景功能拆分成多个服务的思想概念;

微只是一种概念,怎么去实现,后端有后端的时候实践方式,前端也有前端的实践方式;

# 微前端要解决的问题

  • 文件越来越多,文件结构越不受控制,业务开发寻址变得越来越困难。

  • 文件越来越多,开发、构建、部署速度变得越来越慢,开发体验在持续下降。

# 微前端的实践方式

  • iframe:最简单的就是iframe的嵌套,子工程可以使用任意技术栈,需要时直接加一个iframe进行src的指定即可

  • NPM方式:将子应用打成npm包的形式发布源码,打包构建由发布应用的基座进行,然后发布;

  • 采用通用中心路由基座方式,由基座应用来统一加载子应用;

# qinkun微前端框架

对于微前端框架,看了一些文章后,决定一这个框架来进行实践;

Qiankun 声称自己是“一个 微前端 实现,基于 single-spa,但已使 single-spa 可用于生产(production-ready)”。该项目旨在解决由较小的子应用程序组成较大的应用程序时所面临的一些主要问题,例如发布静态资源、集成单个子应用程序、确保子应用程序在开发和部署过程中彼此独立且运行时相互隔离、处理公共依赖性和处理性能问题等。

项目链接:https://github.com/umijs/qiankun (opens new window)

# 实践

我们实践时,基座与子应用均采用vue+vue-router进行;我们只用在主应用中引入qinkun即可,子应用不会引入依赖,但子应用需要暴露生命周期的方法给主应用;

# 创建基座项目

  1. 创建应用
vue create think-free-qkmain
  1. 添加依赖
yarn add qiankun # 或者 npm i qiankun -S
  1. 创建src/components/father.vue
<template>
    <div>我是father</div>
</template>
<script>
    export default {
        name: "father"
    }
</script>
  1. src/router/index.js注解router
import Router from 'vue-router'
export default new Router({
    mode:'history',
    base:'',
    routes: [
        { path: '/', redirect: '/father'},
        { path:'/father', component: ()=> import('@/components/father')}
    ]
})
  1. main.js中进行初始化
import Vue from "vue"
import App from "./App.vue"
import Router from 'vue-router'
import router from "./router"
import { registerMicroApps, start } from "qiankun"

Vue.use(Router);
Vue.config.productionTip = false;

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

// 在主应用中注册子应用
registerMicroApps([
    {
        name: "vue app",
        entry: "//localhost:8083",	// 重点8:对应重点6
        container: '#vue',			// 重点9:对应重点2
        activeRule: '/vue'			// 重点10:对应重点4
    }]
);
// 启动
start();
  1. 采用@vue/cli-service后配置webpack需要在项目根目录下添加vue.config.js
module.exports ={
    devServer: {
        port: 8085,
        headers: {			// 重点1: 允许跨域访问子应用页面
            'Access-Control-Allow-Origin': '*',
        }
    }
}
  1. 修改app.vue中的内容
<template>
  <div class="app">
    <span><router-link to="/">点击跳转到父页面</router-link></span>
    <span><router-link to="/vue">点击跳转到子页面</router-link></span>
    <router-view />
    <div id="vue"></div>	<!-- 重点2:子应用容器 id -->
  </div>
</template>

# 创建子应用

  1. 创建应用
vue create think-free-qkchild
  1. 创建src/components/child.vue
<template>
    <div>我是child</div>
</template>
<script>
    export default {
        name: "child"
    }
</script>
  1. src目录新增public-path.js
if (window.__POWERED_BY_QIANKUN__) {
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
  1. src/router/index.js中添加路由信息
import Router from 'vue-router'
import '../public-path'			// 重点3: 引入public-path文件
export default new Router({
    base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/',			// 重点4
    mode: 'history',			// 重点5
    routes: [
        { path:'/', redirect: '/child'},
        { path: '/child', component: ()=>import('../components/child')}]
})
  1. main.js中处理
import Vue from 'vue';
import App from './App.vue'
import Router from 'vue-router'
import router from "./router/index"

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

Vue.use(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');  // 为了避免根id#app与其他DOM冲突,需要限制查找范围
}

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;
}
  1. vue.config.js配置
const { name } = require('./package');
module.exports = {
    devServer: {
        port: 8083,			// 重点6
        headers: {			// 重点7:同重点1,允许子应用跨域
            'Access-Control-Allow-Origin': '*',
        },
    },
    // 自定义webpack配置
    configureWebpack: {
        output: {
            library: `${name}-[name]`,
            libraryTarget: 'umd',		// 把子应用打包成 umd 库格式
            jsonpFunction: `webpackJsonp_${name}`,
        },
    },
};
  1. app.vue中的修改
<template>
  <div class="app">
    我是子应用的内容
    <router-view />
  </div>
</template>

在启动基座项目think-free-qkmain和子应用think-free-qkchild后,访问http://localhost:8085主应用,即可在主应用中访问到子应用的页面

# 子应用的四个生命周期钩子函数

  1. bootstrap:bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。

  2. mount:应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法。

  3. unmount:应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例。

  4. update:可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效.

# 问题

# router的history与hash模式问题

官方示例中,在创建router的时候都是给的mode:history,当前以上所有的都是以history进行的示例

如果以hash模式是否可行了?当然也是可以的;只需要进行以下几步改造即可:

基座:

  1. 修改注册子应用的路由
  {
      name: "vue app",
      entry: "//localhost:8083",	// 重点8:对应重点6
      container: '#vue',			// 重点9:对应重点2
      activeRule: '/#/vue'			// 重点10:对应重点4
  }]
);
  1. 修改注册router
import Router from 'vue-router'
export default new Router({
  routes: [
    { path: '/', redirect: '/father'},
    { path: '/father', component: () => import('@/components/father') },
    {
      name: 'vue',
      path: '/vue/*',
      component: () => import('@/components/system/layout')
  }]
})

子应用修改

  1. 修改router.js
let prefix = ''
// 判断是 qiankun 环境则增加路由前缀
if(window.__POWERED_BY_QIANKUN__){
    prefix = '/vue'  // 与基座的activeRule一致
}
// 每个routre的path要加上前缀
export default new Router({
    routes: [
      { path: prefix + '/', redirect: prefix + '/child'},
      { path: prefix + '/child', component: ()=>import('../components/child')}]
})

# 如何在某个路由页面加载微应用

主要是在主应用中进行路由映射和子应用开启功能

  1. 主应用注册这个路由时给 path 加一个 *,注意:如果这个路由有其他子路由,需要另外注册一个路由,仍然使用这个组件即可。
const routes = [
  {
    path: '/portal/*',
    name: 'portal',
    component: () => import('../views/Portal.vue'),
  },
];
  1. 微应用的 activeRule 需要包含主应用的这个路由 path
registerMicroApps([
  {
    name: 'app1',
    entry: 'http://localhost:8080',
    container: '#container',
    activeRule: '/portal/app1',
  },
]);
  1. Portal.vue 这个组件的 mounted 周期调用 start 函数,注意不要重复调用
import { start } from 'qiankun';
export default {
  mounted() {
    if (!window.qiankunStarted) {
      window.qiankunStarted = true;
      start();
    }
  },
};

必须保证微应用加载时主应用这个路由页面也加载了

# js框架依赖问题

比如我们在基座需要引入element-ui,在子应用中也需要引入element-ui,因为应用是分别打包部署,所以是没法共用一个引入,但是我们可以采用<srcipt>直接引入外部公共js的方式来引入第三方的公共包,因为浏览器会缓存同一域下的第三方包,这样子应用和基座以及其他子应用不用重复打包公共的js模块;

# 参考来源

https://tech.meituan.com/2020/02/27/meituan-waimai-micro-frontends-practice.html (opens new window)

https://blog.csdn.net/xiongmi_Lin/article/details/112537780 (opens new window)

https://qiankun.umijs.org/zh/faq (opens new window)

https://developer.51cto.com/art/202010/628894.htm (opens new window)

https://segmentfault.com/a/1190000024456492 (opens new window)

最后更新时间: 10/7/2021, 11:32:38 PM