qiankun.js 微前端拆分vue+webpack项目

1,715 阅读3分钟

主要内容

目的:为了将一个大型项目拆分为多个子项目.通俗来说,就是vue项目中,web1为导航栏,web2为门户,web3为系统管理,通过微前端将他们糅合成一个完成的项目.

qiankun.js介绍

qiankun.js是当前最出色的一款微前端实现库, 他帮我们实现了css隔离js隔离项目关联等功能

# 安装qiankun.js
$ npm i qiankun -S

1. 项目主目录结构

共三个vue项目,container属于项目导航部分,web1为门户部分,web2为系统管理部分,container项目中有个subapp文件夹,用来存在web1web2项目(subapp文件夹也可以放外层)。

1.1 结构一:

image.png

1.2 结构二:

image.png

2. 安装qiankun后配置项目加载规则

2.1 在containerapp.vue

<template>
    <div id="nav">
    
        <!-- 定义导航栏,且激活地址 -->
        <router-link to="/">Home</router-link> |
        <router-link to="/w1">web1</router-link> |
        <router-link to="/w2">web2</router-link>
        
        <!-- 新增id为box的元素,将web1中的内容插入到box中 子容器 -->
        <div id="box"></div>
        
        <!-- container(父容器) -->
        <router-view />
    </div>
</template>

2.2 修改home.vue中的内容

<template>
    <div class="home">我是 container 工程</div>
</template>

此时界面如下: image.png

2.3 配置containermain.js

// 引入qiankun.js
import { registerMicroApps, start } from 'qiankun';

// 注册子应用
registerMicroApps([
  {
    name: 'vueApp2',
    entry: '//localhost:8083',
    container: '#box',
    activeRule: '/w2',
  },
  {
    name: 'vueApp1',
    entry: '//localhost:8082',
    container: '#box',
    activeRule: '/w1',
  },
]);

// 开启服务
start();

参数解析: registerMicroApps(apps, lifeCycles?)

  • apps - Array - 微应用的注册信息
  1. name -String - 微应用的名称,必须确保唯一 - 【必选】
  2. entry - String - 微应用的入口 - 【必选】
  3. container - String | HTMLElement - 微应用的容器节点的选择器或者Element实例 - 【必选】
  4. acticeRule - String | (location:Location) => boolean | Array | (location:Location) => boolean - 微应用的激活规则 - 【必选】

3. 在subapp中新建子项目

3.1 新建子项目

# 跳转到subapp目录下
$ cd subapp

# 初始化web1
$ vue init webpack web1

# 初始化web2
$ vue init webpack web2

3.2 配置web1项目,web2同理配置

3.2.1 配置web1main.js

main.js导出自己的生命周期函数

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from "vue";
import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;

let instance = null;
function render() {
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount('#web1') // 框架会拿到完整的dom结构, 所以index.html里面的id也要改一下
}

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

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount() {
  console.log('bootstrap mount');
  render()
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log('bootstrap unmount');
  instance.$destroy()
}

3.2.2 web1中的index.html中修改

index.html为打包模版,因为有多个子应用,所以id不唯一,将divid改为web1

image.png

3.2.3 web1App.vue修改

<template>
    <div id="web1">Welcome Web1 page!</div>
</template>

3.2.4 web1 -> config -> index.js中修改端口号

这里修改后的端口号和主应用中配置的微服务的端口号对应。

module.exports = {
  dev: {
      port:'8082' // web2 改为 8083
  }
}

4. 运行项目

4.1 单独启动

# contianer,web1,web2顶层目录下启动
$ npm run dev

4.2 一起启动

4.2.1 安装npm-run-all

单独依次启动应用比较麻烦,可以统一启动,安装npm-run-all

$ npm i npm-run-all --save-dev

4.2.2 配置contianer下的package.json

"scripts": {
    "serve": "npm-run-all --parallel serve:*",
    "serve:box": "vue-cli-service serve",
    "serve:web1": "cd subapp/web1 && yarn serve",
    "serve:web2": "cd subapp/web2 && yarn serve",
    "build": "npm-run-all --parallel build:*",
    "build:box": "vue-cli-service build",
    "build:web1": "cd subapp/web1 && yarn build",
    "build:web2": "cd subapp/web2 && yarn build"
  },

运行项目即可

4.3 运行项目后请求子应用跨域

image.png 在子应用的build -> webpack.dev.js中修改

devServer: {
    // 由于会产生跨域, 所以加上
    headers: {
      'Access-Control-Allow-Origin': "*"
    },
}

现在的运行结果为: image.png image.png image.png

# 踩坑记录

Q1: webpackJsonp_vue is not defined

image.png

A1: 路由中用到异步懒加载引入改为正常引入

// const Home = resolve => require(['@/views/Home.vue'], resolve);
// const Login = resolve => require(['@/components/Login.vue'], resolve); 
import Home from '@/views/Home.vue';
import Login from '@/components/Login.vue';

Q2: Uncaught Error: application 'vueApp1' died in status LOADING_SOURCE_CODE: [qiankun]: You need to export lifecycle functions in vueApp1 entry

image.png

A2: 答案是缺少libraryTarget:'umd',需要以包的形式打包, 挂载window

image.png

Q3:application 'portal-site' died in status LOADING_SOURCE_CODE: Failed to fetch

image.png

A3: 检查子应用是否启动以及子应用的端口号是否匹配的上

在这里我的问题是:子应用的配置中的端口号为8081,,然而在main.jsdapps配置中出现了8001的问题,草率了...

Q4: application 'portal-site' died in status LOADING_SOURCE_CODE: [qiankun]: Target container with #portalSite not existed while portal-site loading! - 目标容器不存在

  • Target container with #container not existed while xxx loading! - 目标容container一开始就不存在
  • Target container with #container not existed while xxx mounting - 说明刚开始container容器是存在的,后来因为代码改变了document,导致container容器丢失。

image.png

A4: 当前的子应用挂在容器错误

const apps = [
   {
       name:'portal-site',
       entry: "//localhost:8081/portal-site",
       // container: "#portalSite",
       container: "#app",
       activeRule: "/portal-site"
   }
]
export default apps;

Q5: Loading chunk {x} failed.

image.png

A5: 将路由的异步引入改为同步

image.png

【注意】:通过度娘,看到好多人的解释都是路由加载后出现的问题,所以在router -> index.js文件中导航守卫之前加入路由的错误监听,然而并没有解决上面的问题...

// router/index.js
router.onError((error) => {
   const pattern = /Loading chunk (\d) + failed/g;
   const isChunkLoadFailed = error.message.match(pattern);
   const targetPath = router.history.pending.fullPath;
   if (isChunkLoadFailed) {
       router.replace(targetPath)
   }
})

遗留问题

  • 子应用单独开发
  • 打包
  • css隔离
  • js隔离