微前端qiankun简易上手指南

17,149 阅读7分钟

前言

本文主要介绍了微前端 qiankun 环境的搭建,以及如何在主应用中挂载子应用,主应用和子应用之间通信,如何在子应用中接入路由。详细的整理,各种配置文件。分别介绍了 ReactVue 子应用的挂载方法。

如果,之前从未接触过微前端,这应该是个不错的上手项目。项目demo我已经放在 gitee 上面。

🚀Gitee地址

那么,先从什么是微前端 qiankun 说起。

关于qiankun

微前端

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端借鉴了微服务的架构理念,将一个庞大的前端应用拆分为多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用联合为一个完整的应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。

qiankun

qiankun 是一个基于 single-spa 的微前端实现库。

🚀 qiankun 官网地址

应用场景

1.项目的迁移,老项目的改造,更新主要的技术栈

2.公司的小伙伴儿比较多,用啥的都有

搭建环境

我使用的node版本:14.8.0

分别创建三个应用,将 qiankun-base 作为主应用

npm create react-app qiankun-base --template typescript

npm create react-app qiankun-micro-app1 --template typescript

npm create react-app qiankun-micro-app2 --template typescript

相关配置,在每个应用的文件夹,根目录的中新建 .env 文件。

配置不同的端口号。

// qiankun-base应用
PORT=3010
// qiankun-micro-app1应用
PORT=3011
//qiankun-micro-app2应用
PORT=3012

快速上手之前,先看一下官网的 快速上手

在主应用  qiankun-base 安装 qiankun


npm i qiankun -S

在主应用 qiankun-base 的入口文件 index.ts 中注册微应用


import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'micro-app1', // app name registered
    entry: '//localhost:3011',
    container: '#micro-app1',
    activeRule: '/micro-app1',
  },
  {
    name: 'micro-app2',
    entry: '//localhost:3012',
    container: '#micro-app2',
    activeRule: '/micro-app2',
  },
]);

start();

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑。

所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

然后,在主应用中,修改App.tsx,加入 container 容器。


import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      <div id="micro-app1"></div>
      <div id="micro-app2"></div>
    </div>
  );
}

export default App;

主应用中挂载子应用

微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。

React 微应用,相关配置:

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__;
}

2.修改webpack配置

①安装 react-app-rewired

npm install react-app-rewired --save

②在 package.json 文件中,修改启动脚本。

"scripts": {
    "start": "react-app-rewired start",
},

安装react-app-rewired 后,可以重写webpack的配置信息。

③在微应用根目录下,新建 config-overrides.js 文件。

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

module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';

    return config;
  },
  devServer: (_) => {
    const config = _;
    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
}

④修改微应用的入口文件 index.tsx

// @ts-ignore
function render(props) {
  const { container } = props;
// @ts-ignore
  ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}
// @ts-ignore
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}
// @ts-ignore
export async function bootstrap() {
  console.log('[react16] react app bootstraped');
}
// @ts-ignore
export async function mount(props) {
  console.log('[react16] props from main framework', props);
  render(props);
}
// @ts-ignore
export async function unmount(props) {
  const { container } = props;
// @ts-ignore
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}

⑤为了方便区分,修改 qiankun-micro-app1 微应用中的 App.tsx 文件。


import React from "react";
import "./App.css";

function App() {
  return <div className="App">qiankun-micro-app1</div>;
}

export default App;

⑥启动主应用,和两个子应用。

通过主应用配置的 activeRule 匹配到指定的 container 中,成功展示出子应用。

http://localhost:3010/micro-app1

页面展示:

qiankun-micro-app1

http://localhost:3010/micro-app2

页面有个图片没有展示出来,接下来就解决这个问题。

4.解决静态资源不显示的问题

在微应用src文件夹的 index.tsx 入口文件中,导入之前配置的 public-path.js 文件

import './public-path';

刷新页面,成功展示图片

http://localhost:3010/micro-app2

打开控制台在 Elements 元素,可以看到:

<img src="http://localhost:3012/static/media/logo.svg" class="App-logo" alt="logo">

导入 public-path.js 文件之后,图片路径变成了完整的url路径:

http://localhost:3012/static/media/logo.svg

主应用和子应用之间通信

主应用和子应用之间,可能公用一些参数,如何实现传参呢?

修改主应用 qiankun-base 的配置,在 index.tsx 文件中,加入 props 参数。

registerMicroApps([
  {
    name: "micro-app1", // app name registered
    entry: "//localhost:3011",
    container: "#micro-app1",
    activeRule: "/micro-app1",
    props: {
      niceBody: "malena",
      age: 32
    }
  },
  {
    name: "micro-app2",
    entry: "//localhost:3012",
    container: "#micro-app2",
    activeRule: "/micro-app2",
    props: {
      niceBody: "malena",
      age: 32
    }
  }
]);

子应用 qiankun-micro-app1 的入口文件 index.tsx 中,在 mount 方法中,可以获取的主应用传递的props 参数。


// @ts-ignore
export async function mount(props) {
  console.log('[react16] props from main framework', props);
  // render(props);
  // @ts-ignore
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });
  // @ts-ignore
  // props.setGlobalState(state);
}

如果主应用中参数,发生改变。微应用中,也能接收到改变。

譬如,设置一个定时器,2秒后将state数据中的age从32改为34

主应用 入口文件 index.tsx

import { initGlobalState, MicroAppStateActions } from 'qiankun';

const state = {
  name: 'malena morgan'
}

// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});

setTimeout(() => {
  actions.setGlobalState({ ...state, age: 34});
}, 2000);
actions.offGlobalStateChange();

微应用 入口文件 index.tsx

// @ts-ignore
export async function mount(props) {
  console.log('[react16] props from main framework', props);
  // render(props);
  // @ts-ignore
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });
}

如果在微应用中,改变 state 的值,主应用中也能拿到

// @ts-ignore
export async function mount(props) {
  console.log('[react16] props from main framework', props);
  // render(props);
  // @ts-ignore
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
      // @ts-ignore
    setTimeout(() => {
      props.setGlobalState({...state, age: 36})
    }, 2000);
  });
}

qiankun接入vue3

首先,新建一个vue项目

安装脚手架

npm install -g @vue/cli 

创建vue3项目

vue create qiankun-micro-vue3-app3

安装typescript

cd qiankun-micro-vue3-app3
vue add typescript

进行配置:

修改 vue.config.js 文件

// @ts-nocheck
const { name } = require('./package.json');
module.exports = {
  devServer:{
    port: 3013,
    headers:{
      'Access-Control-Allow-Origin': '*',
    }
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
    },
  },
}

在 src 文件夹下,新建 public-path.js 文件

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

修改 main.ts 入口文件

// @ts-nocheck
import "./public-path";
import { createApp } from "vue";
import Vue from "vue";
import App from "./App.vue";
let instance = null;

function render(props = {}) {
  const { container } = props;
  instance = createApp(App);
  instance.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('vue3-app')
  console.log(props)
  render(props);
  instance.config.globalProperties.$onGlobalStateChange =
    props.onGlobalStateChange;
  instance.config.globalProperties.$setGlobalState = props.setGlobalState;
}

export async function unmount() {
  instance.unmount();
  instance._container.innerHTML = "";
  instance = null;
}

在 qiankun-base 主应用中,加载微应用

registerMicroApps([
  {
    name: "micro-app1", // app name registered
    entry: "//localhost:3011",
    container: "#micro-app1",
    activeRule: "/micro-app1",
    props: {
      niceBody: "malena",
      age: 32
    }
  },
  {
    name: "micro-app2",
    entry: "//localhost:3012",
    container: "#micro-app2",
    activeRule: "/micro-app2",
    props: {
      niceBody: "malena",
      age: 32
    }
  },
  {
    name: "micro-vue3-app3",
    entry: "//localhost:3013",
    container: "#micro-vue3-app3",
    activeRule: "/micro-vue3-app3",
    props: {
      niceBody: "malena",
      age: 32
    }
  }
]);

如你所愿,如下所示vue3应用

到此为止,完成了各个子应用之间的切换,那么子应用中各个页面的切换,又该怎么做呢?这就是下面要讲到的。

react子应用接入路由

首先,当然要安装路由 react-router-dom

npm install react-router-dom --save

在子应用 index.tsx 入口文件中,引入路由

import { BrowserRouter } from "react-router-dom";
import "./public-path";
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <BrowserRouter>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </BrowserRouter>
);

// @ts-ignore
function render(props) {
  const { container } = props;
  // @ts-ignore
  ReactDOM.render(
    <BrowserRouter
      basename={window.__POWERED_BY_QIANKUN__ ? "/micro-app2" : "/"}
    >
      <App />
    </BrowserRouter>,
    container
      ? container.querySelector("#root")
      : document.querySelector("#root")
  );
}
// @ts-ignore
if (!window.__POWERED_BY_QIANKUN__) {
  // render({});
}
// @ts-ignore
export async function bootstrap() {
  console.log("[react16] react app bootstraped");
}
// @ts-ignore
export async function mount(props) {
  console.log("[react16] props from main framework", props);
  render(props);
}
// @ts-ignore
export async function unmount(props) {
  const { container } = props;
  // @ts-ignore
  ReactDOM.unmountComponentAtNode(
    container
      ? container.querySelector("#root")
      : document.querySelector("#root")
  );
}

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

在 /src/pages 文件夹下,新建两个页面,命名随意。

修改 App.tsx 文件,配置这两个页面的路由。

import React from 'react';
import logo from './logo.svg';
import './App.css';
import { Routes, Route, Link } from "react-router-dom";
import Mila from './pages/Mila';
import Malena from './pages/Malena';

function App() {
  return (
    <div className="App">
      <Link to={"/"}>home</Link> | 
      <Link to={"/mila"}>micro-app2 mila</Link> | 
      <Link to={"/malena"}>micro-app2 malena</Link>
      <Routes>
        <Route path="/mila" element={<Mila/>} />
        <Route path="/malena" element={<Malena/>} />
      </Routes>
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

正如你所看到的,成功接入路由。

vue3子应用接入路由

第一步,当然也是安装路由

npm install vue-router --save

新建 src\router\index.ts 文件,新建两个页面,在路由文件中配置。


import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import Mila from '../pages/Mila.vue';
import Malena from '../pages/Malena.vue';

const routes: RouteRecordRaw[] = [
  {
    path: '/mila',
    component: Mila
  },
  {
    path: '/malena',
    component: Malena
  }
];
const router = createRouter({
  history: createWebHistory(
    window.__POWERED_BY_QIANKUN__ ? "/micro-vue3-app3" : "/"
  ),
  routes
});
export default router;

然后,在 main.ts 文件中使用

import router from './router/index';

function render(props = {}) {
  const { container } = props;
  instance = createApp(App);
  instance.use(router)
  instance.mount(container ? container.querySelector("#app") : "#app");
}

入口页面 App.vue 中,加入 router-view

<template>
  <router-link to="/mila">mila</router-link> |
  <router-link to="/malena">malena</router-link>
  <router-view />
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
</template>

<script lang="ts">
import { Options, Vue } from 'vue-class-component';
import HelloWorld from './components/HelloWorld.vue';

@Options({
  components: {
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

vue3子应用,也成功接入路由。

http://localhost:3010/micro-vue3-app3/malena

最后的话

以上,如果对你有用的话,不妨点赞收藏关注一下,谢谢 🙏

😊 微信公众号: OrzR3

💖 不定期更新一些技术类,生活类,读书类的文章。