28

vue项目'微前端'qiankun.js的实战攻略

本篇介绍

     关于微前端的大概念大家应该听过太多了, 这里我就大白话阐述一下, 比如我们新建三个vue工程a、b、c, a负责导航模块, b负责列表页面, c负责详情页面, 然后我们可以通过微前端技术把他们组合在一起形成一个完整项目

    本篇文章不会讲述很深入的细节操作, 但会讲述项目搭建到项目上线的全环节, 如果你这些都会了那么其他的问题就不是太大阻碍了。

     一定要明确一点, 微前端在很多场景都是不适用的, 千万不要强行使用这门技术, 在本篇文章里我会一点点的阐述什么场景不适用以及为什么不适用。
    

1. 微前端qiankun.js简简简简介

     qiankun.js是当前最出色的一款微前端实现库, 他帮我们实现了css隔离js隔离项目关联等功能, 文章的后面都会有所涉及的现在就让我们开始实战吧。

2. 本次的项目结构一主二附

     一共三个vue项目, 第一个container项目负责导航模块, 第二个web1第三个web2, container项目里面有个subapp文件夹, 里面存放着web1 & web2两个项目, 这样以后我们可以随便添加web3,web4....都放在subapp文件夹即可。
image.png

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

     在我们的容器项目container里面安装qiankun如下命令:

$ yarn add qiankun # 或者 npm i qiankun -S

     打开container项目的App.vue文件我们把导航重定义一下:
     /w1/w2路由地址分别激活web1工程与web2工程。

 <div id="nav">
  <router-link to="/">Home</router-link> |
  <router-link to="/w1">web1</router-link> |
  <router-link to="/w2">web2</router-link> |
</div>

     我们新增一个id为"box"的元素, 接下来我们引入的web1工程就会插入到这个元素中。

<div id="box"></div>
<router-view />

     把Home.vue页面代码改掉:

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

<script>
export default {
  name: "Home",
};
</script>

<style>
.home {
  font-size: 23px;
}
</style>  

此时的页面是这个样子的:
image.png

     打开container项目的main.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();

参数解析:

  1. name: 微应用的名称,微应用之间必须确保唯一, 方便后期区分项目来源。
  2. entry: 微应用的入口也就是当满足条件的时候, 我要激活的目标微应用的地址(也可以是其他形式比如html片段, 但本篇主要讲url地址这种形式)。
  3. container: 激活微应用的时候我们要把这个目标微应用放在哪里, 上面代码的意思就是把激活的微应用放在id为'box'的元素里面。
  4. activeRule:微应用的激活规则(有很多种写法甚至是函数形式), 上面代码就是当路由地址为/w1时激活。

4. 配置子项目main.js

     以配置web1项目为例, web2与其类似, 在main.js中导出自己的生命周期函数。

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() {
  render()
}

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

web1 >public >index.html中的div元素id从app改为web1, 因为要多个项目合成一个项目, 所以id最好还是不要重复。

<!DOCTYPE html>
<html lang="">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
</head>

<body>
  <div id="web1"></div>
</body>

</html>

web1vue.config.js

module.exports = {
  devServer: {
        port: 8082, // web2里面改成8083
    },
}

     现在我们要分别进入container, web1web2里面运行yarn serve命令, 但是这样运行命令真的好麻烦, 接下来我就介绍一种更工程化的写法。

5. npm-run-all

     npm-run-all是用来通过执行一条语句来达到执行多条语句的效果的插件。

$ npm install npm-run-all --save-dev

# or 

$ yarn add npm-run-all --dev

改装我们的container工程中的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"
  },

我解释一下:
运行: yarn serve 系统会执行scripts 里面所有的头部为serve:的命令, 所以就会实现一个命令运行三个项目, 这里顺手把build命令也写了。

其他扩展玩法:

  1. serial: 多个命令按排列顺序执行,例如:npm-run-all --serial clean lint build:**
  2. continue-on-error: 是否忽略错误,添加此参数 npm-run-all 会自动退出出错的命令,继续运行正常的
  3. race: 添加此参数之后,只要有一个命令运行出错,那么 npm-run-all 就会结束掉全部的命令
上述准备工作都做完了, 我们可以启动项目试试了。

6. 请求子项目竟然跨域

     运行起来会发现报错了:
image.png

需要在web1web2两个项目vue.config.js里加上如下配置就不报错了:

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

之所以会有这种跨域的报错是因为qiankun内部使用fetch请求的资源, 当前毕竟是启动了三个不同的node服务, 外部html页面请求其资源还是会跨域的, 所以需要设置允许所有源。

我们为web1web2设置一下样式, 结果如下:

image.png
image.png
image.png

  • 但这些仅仅是个开始而已, 因为各种问题马上纷至沓来。

7. 区分在是否在主应用内

     我们有时候需要单独开发web1, 此时我们并不依赖container项目, 那么我们就要把main.js改装一下:

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')
}

if (window.__POWERED_BY_QIANKUN__) {
  window.__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}
export async function bootstrap() {
  console.log('bootstrap');
}
export async function mount() {
  render()
}
export async function unmount() {
  instance.$destroy()
}

逐句解释:

  1. window.__POWERED_BY_QIANKUN__: 当前环境是否为qiankun.js提供。
  2. window.__webpack_public_path__: 等同于 output.publicPath 配置选项, 但是他是动态的。
  3. window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__:qiankun.js注入的公共路径。

判断当前环境为单独开发的环境就直接执行render方法, 如果是qiankun的容器内, 那么需要设置publicPath, 因为qiankun需要把每个子应用都区分开, 然后引入容器项目内, 这样我们就可以单独开发web1项目了。

8. 子应用路由跳转与vue-router的异步组件小bug

     在配置router的时候我们经常会将页面写成异步加载:

component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),

web2项目中的home页面, 我增加一个按钮跳到about页面:

<template>
  <div class="home">
    <button @click="jump">点击跳转到about页面</button>
  </div>
</template>

<script>
export default {
  methods: {
    jump() {
      // this.$router.push("/web2/about");
      window.history.pushState(null, null, "/w2/about");
    },
  },
};
</script>

     上述代码不可以直接用this.$router.push, 这样会与qiankun.js的路由分配冲突, 官网上说会出现404这种情况, 所以建议我们直接用 window.history.pushState

     但是这中写法在当前版本qiankun.js里面可能会有如下错误:
image.png

     这是由于动态设置的publicPath并不能满足加载异步组件chunk, 需要我们如下配置一番:(web2->vue.config.js)

publicPath: `//localhost: 8083`

就可以正常加载这个页面了:
image.png

并且此时直接刷新当前url也还可以正确显示about页面。

9. 区分开发与打包

     前面几条说的都是开发相关的设置, 这里我们要开始介绍打包的配置了, 这里会介绍原理与做法, 不会做的很细所以具体的项目开发还是要好好的封装一番。

我这里先把nginx简单配置一下, 让这个包能用。

location /ccqk/web1 {
    alias   /web/ccqk/web1;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
}

location /ccqk/web2 {
    alias   /web/ccqk/web2;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
}

location /ccqk {
    alias   /web/ccqk/container;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
}

由于我之前有项目在服务器上为了方便区分, 随便写了个ccqk前缀, 那么现在目标很明确了, 我需要打一个叫ccqk的文件夹, 里面有三个包containerweb1web2

第一步: 确立打包路径
  • container -> vue.config.js

    module.exports = {
    outputDir: './ccqk/container',
    publicPath: process.env.NODE_ENV === "production" ? `/ccqk` : '/',
    };
  • web1 -> vue.config.js
const packageName = require('./package.json').name;
const port = 8082
module.exports = {
    outputDir: '../../ccqk/web1',
    publicPath: process.env.NODE_ENV === "production" ? '/ccqk/web1' : `//localhost:${port}`,
    devServer: {
        port,
        headers: {
            'Access-Control-Allow-Origin': "*"
        }
    },
    configureWebpack: {
        // 需要以包的形式打包, 挂载window上
        output: {
            library: `${packageName}-[name]`,
            libraryTarget: 'umd',
            jsonpFunction: `webpackJsonp_${packageName}`,
        },
    },
    chainWebpack: config => {
        config.plugin("html").tap(args => {
            args[0].minify = false;
            return args;
        });
    }
};
  • web2 -> vue.config.json

    const packageName = require('./package.json').name;
    const port = 8083
    module.exports = {
    outputDir: '../../ccqk/web2',
    publicPath: process.env.NODE_ENV === "production" ? '/ccqk/web2' : `//localhost:${port}`,
    devServer: {
        port,
        headers: {
            'Access-Control-Allow-Origin': "*"
        }
    },
    configureWebpack: {
        output: {
            library: `${packageName}-[name]`,
            libraryTarget: 'umd',
            jsonpFunction: `webpackJsonp_${packageName}`,
        },
    },
    chainWebpack: config => {
        config.plugin("html").tap(args => {
            args[0].minify = false;
            return args;
        });
    }
    };

    知识点注意解释:

  1. output.library: 配置导出库的名称, 如果libraryTarget设置为'var'那么主应用可以直接用window访问到。
  2. output.libraryTarget:这里设置为umd意思是在 AMD 或 CommonJS 的 require 之后可访问。
  3. output.jsonpFunction:webpack用来异步加载chunk的JSONP 函数。
  4. chainWebpack: 用来修改webpack的配置, 配置不进行压缩。
第二步: 配置路由路径
  • web2 -> router ->index.js

    const router = new VueRouter({
    mode: "history",
    base: process.env.NODE_ENV === "development" ? '/w2' : '/ccqk/w2',
    routes,
    });

10. css隔离

image.png
     这里的隔离并不是完美的, 想要了解更详细的内容可以看看我的往期文章带你走进-\>影子元素(Shadow DOM)&浏览器原生组件开发(Web Components API ), 看完你就会完全理解为啥不完美。

11. js隔离

     在多应用场景下,每个微应用的沙箱都是相互隔离的,也就是说每个微应用对全局的影响都会局限在微应用自己的作用域内。比如 A 应用在 window 上新增了个属性 test,这个属性只能在 A 应用自己的作用域通过 window.test 获取到,主应用或者其他微应用都无法拿到这个变量。

     我这里就不秀源码不扯大概念, 直接来干货原理, qiankun会在子应用激活的时候为其赋予一个代理后的window对象, 用户操作这个window对象的每一步都会被记录下来, 方便在卸载子应用时还原全局window对象, 你要问如何替换的window对象, 其实它是用withevel来实现的替换, 并且比如jq在执行前为了提高效率都会把window对象传入函数里使用, 那么这里直接传入代理window就都ok了, 电脑越写越卡就不扯太多了。

     所以其实使用了微前端技术方案是要付出一定的成本的, 代码速度肯定是有所降低。

12. 康威定律

  • 第一定律 组织沟通方式会通过系统设计表达出来。
  • 第二定律 时间再多一件事情也不可能做的完美,但总有时间做完一件事情。
  • 第三定律 线型系统和线型组织架构间有潜在的异质同态特性。
  • 第四定律 大的系统组织总是比小系统更倾向于分解。

     只有最适合的组织模式, 没有绝对的模式, 比如一个团队想要试试微前端, 那么其实如果你是个移动端的商城项目, 没什么必要使用微前端, 如果是个小中型的后台系统, 也不是很推荐, 除非你们是一个长期维护并且模块繁多, 或者是你想在这个项目的基础上另启一个项目做, 那么微前端将是一把神器。

end.

这次就是这样, 希望与你一起进步。


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者