微前端——乾坤qiankun(小案例)

一、什么是微前端

微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。微前端的核心在于拆,拆完后在合

二、为什么使用微前端
  • 不同团队间开发同一个应用技术栈不同
  • 希望每个团队都可以独立开发,独立部署
  • 项目中还需要老的应用代码

我们可以将一个应用划分成若干个子应用,将子应用打包成一个个的 lib 。当路径切换 时加载不同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了!从而解决了前端协同开发问题。

三、qiankun框架

文档地址:https://qiankun.umijs.org/zh

2018 年 Single-SPA 诞生了, single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案 ( 本身没有处理样式隔离, js 执行隔离 ) 实现了路由劫持和应用加载。

2019 年 qiankun 基于 Single-SPA, 提供了更加开箱即用的 API ( single-spa + sandbox + import-html-entry ) 做到了,技术栈无关、并且接入简单(像 i frame 一样简单)。

四、qiankun框架实例

这里我们打算建立三个项目进行实操,一个Vue项目充当主应用,另一个Vue和React应用充当子应用

1、创建三个应用
1)创建基座
vue create qiankun-base
2)创建子应用1
 vue create qiankun-vue
3)创建子应用2
 cnpm install -g create-react-app
 create-react-app qiankun-react
  • 三个项目

基座:qiankun-base 子应用:qiankun-vue、qiankun-react

2、项目配置(主要)

1)基座qiankun-base配置

项目创建好后我们首先进行主应用qiankun-base的配置,进入man.js文件进行配置, 在main.js中加入以下代码,要注意的是,entry这项配置是我们两个子项目的域名和端口,我们必须确保两字子项目运行在这两个端口上面,container就是我们的容器名,就是我们子应用挂载的节点,相当于Vue项目里面的app节点,activeRule就是我们的激活路径,根据路径来显示不同的子应用。

  • 引入qiankun插件

    yarn add qiankun 或者 npm i qiankun -S
    
  • main.js配置

// 引入qiankun

import { registerMicroApps, start } from 'qiankun';
const apps = [
 {
  name: 'vueApp', // 应用的名字
  entry: '//localhost:8081', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
  container: '#vue', // 容器名(此项目页面中定义的容器id,用于把对应的子应用放到此容器中)
  activeRule: '/vue', // 激活的路径
  props: { a: 1 } // 传递的值(可选)
 },
 {
  name: 'reactApp',
  entry: '//localhost:20000', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
  container: '#react',
  activeRule: '/react',
  }
]
registerMicroApps(apps); // 注册应用
start({
  prefetch: false // 取消预加载
});// 开启
  • 配置完之后我们去到qiankun-base的app.vue文件进行主应用的页面编写,这里我安装了element-ui来进行页面美化

    npm i element-ui -S
    
  • 在main.js中引入element-ui:

    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';
    Vue.use(ElementUI);
    
  • 修改app.vue的组件代码

<template>
  <div id="app">
   <el-menu :router="true" mode="horizontal"> 
    <!--基座中可以放自己的路由-->
    <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-item index="/react">react应用</el-menu-item>
    </el-menu>
   <router-view></router-view>
   <div id="vue"></div>
   <div id="react"></div>
  </div>
</template>
  • router.js代码
 import Vue from 'vue'
 import VueRouter from 'vue-router'
 import Home from '../views/Home.vue'
 Vue.use(VueRouter)
 const routes = [
  {
      path: '/',
      name: 'Home',
      component: Home
  },
{
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
   }
 ]

 const router = new VueRouter({
   mode: 'history',
  // base: process.env.BASE_URL,
   base: '',
   routes
 })

 export default router

2)子应用qiankun-vue配置

  • main.js配置
 import Vue from 'vue'
 import App from './App.vue'
 import router from './router'

// Vue.config.productionTip = false

 let instance = null
 function render(props) {
  instance = new Vue({
    router,
    render: h => h(App)
   }).$mount('#qkApp'); // 这里是挂载到自己的html中  基座会拿到这个挂载后的html 将其插入进去
 }

if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
 }
if (!window.__POWERED_BY_QIANKUN__) { // 默认独立运行
   render();
 }

 // 父应用加载子应用,子应用必须暴露三个接口:bootstrap、mount、unmount
  // 子组件的协议就ok了
export async function bootstrap(props) {

 };

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

 export async function unmount(props) {
   instance.$destroy();
 }
  • router.js配置
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)
const routes = [
 {
  path: '/',
  name: 'Home',
  component: Home
 },
{
  path: '/about',
  name: 'About',
  component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
 }
]

const router = new VueRouter({
 mode: 'history',
 base: '/vue',
 routes
})

export default router
  • Vue.config.js配置

在子应用的根目录下面新建一个Vue.config.js文件

module.exports = {
lintOnSave: false,  // 关闭eslint检测
devServer: {
    port: 8080,//这里的端口是必须和父应用配置的子应用端口一致
    headers: {
        //因为qiankun内部请求都是fetch来请求资源,所以子应用必须允许跨域
        'Access-Control-Allow-Origin': '*'
    }
},
configureWebpack: {
    output: {
        //资源打包路径
        library: 'vueApp',
        libraryTarget: 'umd'
      }
   }
}
3)子应用qiankun-react配置
  • src目录下index.js文件

     import React from 'react';
     import ReactDOM from 'react-dom';
     import './index.css';
     import App from './App';
    
     function render(){
      ReactDOM.render(
       <React.StrictMode>
       <App />
      </React.StrictMode>,
       document.getElementById('root')
    );
    }
    
    if(!window.__POWERED_BY_QIANKUN__){
      render();
    }
    
     export async function bootstrap(){
    
     }
    export async function mount() {
      render()
    }
     export async function unmount(){
       ReactDOM.unmountComponentAtNode( document.getElementById('root'));  // 卸载节点
     }
    
  • config-overrides.js配置
    先引入react-app-rewired,在修改package.json启动命令

    npm install react-app-rewired
     修改package.json启动命令
    
     "scripts": {
      "start": "react-app-rewired start",
      "build": "react-app-rewired build",
      "test": "react-app-rewired test",
      "eject": "react-app-rewired eject"
     },
    
  • 再进行dev以及打包的配置,根目录下创建config-overrides.js

    module.exports = {
      webpack: (config) => {
      config.output.library = 'reactApp';
      config.output.libraryTarget = 'umd';
      config.output.publicPath = 'http://localhost:20000/';   // 此应用自己的端口号
      return config;
     },
     devServer: (configFunction) => {
      return function (proxy, allowedHost) {
          const config = configFunction(proxy, allowedHost);
          config.headers = {
              "Access-Control-Allow-Origin": '*'
          }
          return config
       }
     }
    }
    
3、注意点

1)如何在主应用的某个路由页面加载微应用

react + react-router 技术栈的主应用:只需要让子应用的 activeRule 包含主应用的这个路由即可。

vue +vue-router 技术栈的主应用:

例如:主应用需要在login页面登录,登录成功后跳转到main后台管理界面,在main管理界面下可以显示子应用。

修改主应用router.js:

// 如果这个路由有其他子路由,需要另外注册一个路由,任然使用这个组件即可。
// 本案例就是有子路由,所以需要才后面重新定义main页面的路由

 const routes = [
{
    path: '/',
    name: 'Login',
    component: Login
},
{
    path: '/main',
    name: 'Main',
    component: Main,
    children: [
        {
            path: '/home',
            name: 'Home',
            component: Home
        },
        {
            path: '/about',
            name: 'About',
            // route level code-splitting
            // this generates a separate chunk (about.[hash].js) for this route
            // which is lazy-loaded when the route is visited.
            component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
        }
    ]
 },
 {
    path: '/main/*',
    name: 'Main',
    component: Main,
  }
]
  • 修改主应用main.js的文件:

      // 子应用的 activeRule 需要包含主应用的这个路由 path
    const apps = [
     {
      name: 'vueApp', // 应用的名字
      entry: '//localhost:8081', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
      container: '#vue', // 容器名
      activeRule: '/main/vue', // 激活的路径
     props: { a: 1 }
    },
    {
      name: 'reactApp',
      entry: '//localhost:20000', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
      container: '#react',
      activeRule: '/main/react',
       }
    ]
    registerMicroApps(apps); // 注册应用
    
  • 修改主应用main.vue页面代码:

 // 在 Main.vue 这个组件的 mounted 周期调用 start 函数,注意不要重复调用。
 <template>
  <div class="main-content">
   <el-menu :router="true" mode="horizontal">
  <!--基座中可以放自己的路由-->
  <el-menu-item index="/home">Home</el-menu-item>
  <el-menu-item index="/about">About</el-menu-item>
  <!--引用其他子应用-->
  <el-menu-item index="/main/vue">vue应用</el-menu-item>
  <el-menu-item index="/main/react">react应用</el-menu-item>
  </el-menu>
   <router-view></router-view>
  <div id="vue"></div>
  <div id="react"></div>
 </div>
</template>

 <script>
 import { start } from "qiankun";

export default {
  name: "Main",
  mounted() {
   if (!window.qiankunStarted) {
      window.qiankunStarted = true;
      start();
   }
 },
};
</script>
常见问题
1、资源404

用绝对路径,不用用相对路径,例如

 <img src="./img/logo.jpg">
  <!-- 改为 -->
 <img src="/img/logo.jpg">
2、css污染

qiankun 只能解决子项目之间的样式相互污染,不能解决子项目的样式污染主项目的样式
冲突的样式,采用BEM命名方式

3、不使用position: fixed
4、给 body 、 document 等绑定的事件,请在 unmount 周期清除
5、子应用router.js设置base
6、子应用中的route信息里不要有''或者'*'之类的判空。
7、子项目之间跳转传入父项目路由对象提供调用
8、父子项目通讯:
 // 主项目初始化
 import { initGlobalState } from 'qiankun';
 const actions = initGlobalState(state);
// 主项目项目监听和修改
actions.onGlobalStateChange((state, prev) => {
 // state: 变更后的状态; prev 变更前的状态
 console.log(state, prev);
});

actions.setGlobalState(state);

// 子项目监听和修改
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
   // state: 变更后的状态; prev 变更前的状态
   console.log(state, prev);
  });
   props.setGlobalState(state);
 }
9、销毁子项目
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = ""; //新增这一行代码
  instance = null;
  router = null;
}
10、更新子项目

子应用,需要增加 update 钩子以便主应用手动更新微应用

export async function update(props) {
  render(props);
 }

主应用,直接调用子应用实例的 update 方法即可

this.microApp.update({
  store: {
      name: 'licc',
      age: 18,
   }
});
11、手动 加载/卸载 微应用
import { loadMicroApp } from 'qiankun';

// 因为loadMicroApp()返回子应用的实例,拿一个全局变量接收后续可进行其他操作如:手动卸载子应用
this.microApp = loadMicroApp(
 {
name: 'sub-vue',
entry: 'http://localhost:7777/subapp/sub-vue',
container: '#apassMicroTemplateConfig',
props: {
  routerBase: '/index/config/template/edit',
  getGlobalState: microAppStore.getGlobalState,
  sheetId: '2133123123'
   }
},
{
// sandbox - boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean } - 可选,是否开启沙箱,默认为 true
sandbox: { strictStyleIsolation: true },
// singular - boolean | ((app: RegistrableApp<any>) => Promise<boolean>); - 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 false
singular: true
 }
)

 // 封装卸载子应用的函数
private unmountMicroApp () {
 if (this.microApp) {
  this.microApp.mountPromise.then(() => {
   this.microApp.unmount()
    })
   }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容