基于qiankun的微前端落地方案

一、为什么选择qiankun作为微前端落地方案? 

经过调研,现在市场上微前端方案大概三种:

(1)iframe:关于不使用iframe作为微前端的方案,已经有很多前辈出来分析了,这里直接贴上Why Not Iframe的链接,大家有想了解的可以看下。其中,弹窗等UI组件不能居中显示,是放弃此方案的主要原因

(2)EMP:此方案放弃的原因是目前使用人数较少,怕踩到一些坑找不到好的解决方案。有想了解此方案的可以看下此处的官网介绍。

(3)qiankun:此方案属于国内比较早的微前端落地方案,目前使用人数也比较多

二、业务场景示意图及代码库地址

1.整体业务示意图:

2.代码仓库地址(欢迎star)

a.主应用:

cli模板:creact-react-app

github地址:GitHub - wangliang101/qiankun-react-main: 微前端qiankun实现react主应用

b.react子应用:

cli模板:creact-react-app

github地址:GitHub - wangliang101/qiankun-react-sub: 微qiankun实现react子项目

c.vue子应用

cli模板:vue-cli

github地址:GitHub - wangliang101/qiankun-vue-sub: 微前端qiankun实现vue子项目

三、qiankun接入

主应用(基座)和子应用1使用create-react-app搭建,子应用2使用vue-cli搭建

1.主应用

a.初始化项目

create-react-app react-main

b.安装qiankun

yarn add qiankun

c.新增两个文件,配置主应用端口及不同环境的子应用入口(环境变量一定要以REACT_APP_开头)

// .env.development.local 开发环境使用

REACT_APP_REACT_SUB=//localhost:3001
REACT_APP_VUE_SUB=//localhost:3002
PORT=3000


// .env.production.local 生成环境中使用

REACT_APP_REACT_SUB=react-sub
REACT_APP_VUE_SUB=vue-sub

d.修改/src/app.js内容,修改为我们需要的布局,并且挂载子应用

import { useState } from 'react';
import { registerMicroApps, start } from 'qiankun';
import './App.css';

// 注册子应用
registerMicroApps([
  {
    name: 'reactApp',
    entry: process.env.REACT_SUB,
    container: '#container',
    activeRule: '/react',
  },
  {
    name: 'vueApp',
    entry: process.env.VUE_SUB,
    container: '#container',
    activeRule: '/vue',
  },
]);
// 启动 qiankun
start();

function App() {
  const [activeId, setActiveId] = useState(1);
  // 修改默认布局,增加子应用挂在容器“#container”
  return (
    <div className="app">
      <header className="app_header"></header>
      <div className="app_body">
        <aside className="menu">
          <div className={`menu_button ${activeId === 1 && 'active'}`} onClick={() => setActiveId(1)}>react-sub</div>
          <div className={`menu_button ${activeId === 2 && 'active'}`} onClick={() => setActiveId(2)}>vue-sub</div>
        </aside>
        <div id="container" className="content"></div>
      </div>
    </div>
  );
}

export default App;

e.修改主应用样式

.app{
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.app_header{
  height: 60px;
  background-color:tan;
}

.app_body{
  display: flex;
  flex: 1;
}

.menu{
  width: 300px;
  background-color: steelblue;
  display: flex;
  flex-direction: column;
}

.menu_button{
  height: 50px;
  line-height: 50px;
  text-align: center;
  background-color: wheat;
  border: 1px solid darkcyan;
}

.menu_button:hover{
  background-color: darkseagreen;
  
}

.menu_button.active{
  background-color: darkseagreen;
}


.content{
  background-color: thistle;
  flex: 1;
}

2.react 子应用

 a.初始化react子项目

create-react-app react-sub

b.安装react-app-rewired

# 为了不reject,所以使用react-app-rewired

yarn add react-app-rewired


# 安装react-router-dom,qiankun需设置 history 模式路由的 base

yarn add react-router-dom

c.新增.env文件,端口要和主应用配置中的一致

// .env文件

PORT=3001

d.新增/src/public-path.js文件

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

e.修改子应用布局并配置basename

import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';
import Home from './pages/home';
import About from './pages/about'
import './App.css';

function App() {
  return (
    <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'}>
      <div className="app_react_sub">
        <header className="app_react_sub_header">
          <NavLink className="button" to="/">HOME</NavLink>
          <NavLink className="button" to="/about">ABOUT</NavLink>
        </header>
        <div className="app_react_sub_body">
          <Routes>
            <Route path='/' element={<Home />} />
            <Route path='/about' element={<About />} />
          </Routes>
        </div>
      </div>
    </BrowserRouter>
  );
}

export default App;

f.修改子应用样式

.app_react_sub{
  height: 100%;
  display: flex;
  flex-direction: column;
}

.app_react_sub_header{
  height: 100px;
  line-height: 100px;
  display: flex;
  justify-content: center;
}

.button:nth-child(1){
  margin-right: 50px;

}

.app_react_sub_body{
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: lightsalmon;
  font-size: 30px;
}

g.修改/src/index.js文件,子应用导出相应的生命周期钩子

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';


import './public-path';


function render(props) {
  const { container } = props;
  ReactDOM.render(<App />, container ? container.querySelector('#react-sub-root') : document.querySelector('#react-sub-root'));
}

if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

export async function bootstrap() {
  console.log('[react16] react app bootstraped');
}

export async function mount(props) {
  console.log('[react16] props from main framework', props);
  render(props);
}

export async function unmount(props) {
  const { container } = props;
  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();

h.新增config-overrides.js,覆盖create-react-app的webpack配置

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

module.exports = {
  webpack: config => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    return config;
  },
  devServer: (configFunction) => {
    return (proxy, allowedHost) => {
      const config = configFunction(proxy, allowedHost);
      config.historyApiFallback = true;
      config.open = false;
      config.hot = false;
      config.watchContentBase = false;
      config.liveReload = false;
      config.headers = {
        'Access-Control-Allow-Origin': '*',
      };
      return config;
    }
  }
}

I.修改package.json下script

  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },

3.vue子应用

 a.初始化项目

vue create vue-sub

b.安装vue-router

yarn add vue-router@4

 c.新增.env文件,端口要和主应用配置中的一致

// .env文件

PORT=3002

 d.新增/src/public-path.js文件

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

e.修改子应用布局(/src/App.vue)

<template>
  <div class="app_vue_sub">
    <header class="app_vue_sub_header">
      <router-link class="button" to="/">HOME</router-link>
      <router-link class="button" to="/about">ABOUT</router-link>
    </header>
    <div class="app_vue_sub_body">
      <router-view/>
    </div>
  </div>
</template>

<script>

export default {
  name: 'App',
  
}
</script>

<style>
.app_vue_sub{
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  height: 100%;
  display: flex;
  flex-direction: column;
}

.app_vue_sub_header{
  height: 100px;
  line-height: 100px;
  display: flex;
  justify-content: center;
}

.button:nth-child(1){
  margin-right: 50px;
}

.app_vue_sub_body{
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: peru;
  font-size: 30px;
}
</style>

f.新增路由并配置basename

// /src/router/index.js
import { createRouter, createWebHashHistory} from 'vue-router';
import Home from '../pages/home';
import About from '../pages/about';

const router = createRouter({
  history: createWebHashHistory(window.__POWERED_BY_QIANKUN__ ? '/vue' : '/'),
  routes: [
    {path: '/', component: Home},
    {path: '/about', component: About}
  ]
})

export default router

g.修改/src/main.js文件,子应用导出相应的生命周期钩子

import { createApp } from 'vue'
import App from './App.vue';
import route from './router';


let router = null;
let instance = null;


function render(props = {}) {
  const { container } = props;
  router = route;


  instance = createApp(App);
  instance.use(router);
  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('[vue] props from main framework', props);
  render(props);
}
export async function unmount() {
  console.log('instance', instance)
  instance.unmount();
  instance._container.innerHTML = '';
  instance = null;
  router = null;
}


h.新增vue.config.js,修改webpack配置

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

四、主应用和子应用通信

此主主要是测试主子应用显示初始状态及主/子应用修改全局状态后,其他应用是否能正常监听及显示

1.主应用

a.新建src/store/index.js,此处主要定义全局初始状态及一个获取全局状态的方法

import { initGlobalState } from 'qiankun';
const initState = {
  name: 'react-main',
  value: 'react-main date'
}
// 初始化 state
const actions = initGlobalState(initState);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log('main store',state, prev);
  for(const key in state){
    initState[key] = state
  } 
});
// actions.setGlobalState(state);
// actions.offGlobalStateChange();

// 获取全局状态
actions.getGlobalState = (key) => {
   return initState[key] ?  initState[key] : initState
}

export default actions;

b.主应用显示全局状态及修改全局状态,在src/App.js修改

import { useState, useEffect } from 'react';
import { registerMicroApps, start } from 'qiankun';
import './App.css';
import storeAction from './store';

......

function App() {
  const [activeId, setActiveId] = useState(1);
  const [headerText, setHeadetText] = useState(storeAction.getGlobalState())
  
  const handleClick = (id) => {
    window.history.pushState(null, null, id === 1 ? '/react' : '/vue')
    setActiveId(id)
  }
  // 修改默认布局,增加子应用挂在容器“#container”

  const changeGlobalData = () => {
     storeAction.setGlobalState({
       value: 'react-main change data'
     }) 
  }

  useEffect(() => {
    storeAction.onGlobalStateChange((state, pre) => {
      setHeadetText(state)
    })
    return () => {
      storeAction.offGlobalStateChange()
    }
  }, [])

  return (
    <div className="app">
      <header className="app_header">
        <span>{headerText.name}:{headerText.value}</span>
        <button onClick={changeGlobalData}>改变主应用数据</button>
      </header>
      <div className="app_body">
        <aside className="menu">
          <div className={`menu_button ${activeId === 1 && 'active'}`} onClick={() => handleClick(1)}>react-sub</div>
          <div className={`menu_button ${activeId === 2 && 'active'}`} onClick={() => handleClick(2)}>vue-sub</div>
        </aside>
        <div id="container" className="content"></div>
      </div>
    </div>
  );
}

export default App;

2.react子应用

a.修改src/index.js在render时,将设置及监听全局状态的方法传递给App

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';


import './public-path';

// 在render时,将设置及监听全局状态的方法传给App组件
function render(props) {
  const { container, setGlobalState, onGlobalStateChange } = props;
  ReactDOM.render(<App setGlobalState={setGlobalState} onGlobalStateChange={onGlobalStateChange}/>, container ? container.querySelector('#root') : document.querySelector('#root'));
}

......

b.在home中显示并增加修改全局状态的方法

import React, {useState, useEffect} from 'react';

  const Home = ({onGlobalStateChange, setGlobalState}) => {

  const [text, setText] = useState('');

  const changeGlobalData = () => {
    setGlobalState && setGlobalState({
      value: 'react-sub change data'
    }) 
  }

  useEffect(() => {
    onGlobalStateChange && onGlobalStateChange((state, pre) => {
      setText(state)
    })
  }, [onGlobalStateChange])

  return(
    <div style={{display: 'flex', flexDirection: 'column'}}>
      <div>react-sub home</div>
      <div>主应用数据:{text.value}</div>
      <div onClick={changeGlobalData}><button>修改主应用数据</button></div>
    </div>
  )
}

export default Home;

3.vue子应用

a.修改src/main.js在render时,将设置及监听全局状态的方法挂在vue实例上

import { createApp } from 'vue'
import App from './App.vue';
import route from './router';


let router = null;
let instance = null;


function render(props = {}) {
  const { container } = props;
  router = route;
  instance = createApp(App);
  instance.use(router);
  instance.config.globalProperties.$setGlobalState = props.setGlobalState;
  instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange;
  instance.mount(container ? container.querySelector('#app') : '#app');
}

......

b.在home中显示并增加修改全局状态的方法

<template>
  <div>
    <div class='sub_wrap'>vue-sub home</div>
    <div>主应用数据:{{text.value}}</div>
    <div><button @click='changeGlobalData'>修改主应用数据</button></div>
  </div>
</template>

<script>
import { onMounted, getCurrentInstance, ref } from 'vue';

export default {
  name: "Home",
  setup (){
    const { proxy } = getCurrentInstance();
    let text = ref({})
    const changeGlobalData = () => {
      proxy.$setGlobalState && proxy.$setGlobalState({
        value: 'react-sub change data'
      })
    }

    onMounted(() => {
      proxy.$onGlobalStateChange && proxy.$onGlobalStateChange((state) => {
      text.value = state
    })
    })

    return {
      changeGlobalData,
      text
    }
  },
  
}
</script>

<style scoped>
.sub_wrap{
  display: flex;
  flex-direction: column;
}
</style>>

五、部署

项目采用docker部署,项目间通过nginx来代理。有以下两点需要单独说明一下:

  a.子应用build时需要增加public-path,本项目中,react子应用为"/react-sub",vue子应用为"/vue-sub",仅供参考,如果需要更换,主应用中nginx.conf配置也需要同步更换

  b.至于每个项目的打包方案,将不作为本次内容的重点,如果有感兴趣的看下github中对应项目

1.整体服务的docker-compose.yml文件如下(子应用如不需要单独访问,端口可不设置):

version: '2.3'
services:
  react-mian:
    image: wangliang4468/qiankun:react-main-1.0.0
    restart: always
    ports:
      - 5000:80
  react-sub:
    image: wangliang4468/qiankun:react-sub-1.0.0
    restart: always
    ports:
      - 5001:80
  vue-sub:
    image: wangliang4468/qiankun:vue-sub-1.0.0
    restart: always
    ports:
      - 5002:80

2.react-main nginx配置

server {
        listen       80;
        server_name  127.0.0.1;

        charset utf-8;

        location ^~ /react-sub {
            resolver 127.0.0.11 valid=10s;
            set $upstream_server react-sub;
            proxy_pass http://$upstream_server:80;
            rewrite ^/react-sub(.*)$ /$1 break;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header HTTP_X_FORWARDED_FOR $remote_addr;
            proxy_connect_timeout 300s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
        }


        location ^~ /vue-sub {
            resolver 127.0.0.11 valid=10s;
            set $upstream_server vue-sub;
            proxy_pass http://$upstream_server:80;
            rewrite ^/vue-sub(.*)$ /$1 break;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header HTTP_X_FORWARDED_FOR $remote_addr;
            proxy_connect_timeout 300s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
        }

        location ^~ / {
          root /usr/share/nginx/html;
          index  index.html index.htm;
          try_files $uri $uri/ /index.html;
          
          add_header Last-Modified $date_gmt;
          add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
          if_modified_since off;
          expires off;
          etag off;
        }
}

3.react-sub nginx配置

server {
        listen       80;
        server_name  127.0.0.1;

        charset utf-8;

        location ^~ / {
          root /usr/share/nginx/html;
          index  index.html index.htm;
          try_files $uri $uri/ /index.html;
          
          add_header Last-Modified $date_gmt;
          add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
          if_modified_since off;
          expires off;
          etag off;
        }

}

4.vue-sub nginx配置

server {
        listen       80;
        server_name  127.0.0.1;

        charset utf-8;

        location ^~ / {
          root /usr/share/nginx/html;
          index  index.html index.htm;
          try_files $uri $uri/ /index.html;
          
          add_header Last-Modified $date_gmt;
          add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
          if_modified_since off;
          expires off;
          etag off;
        }

}

六.使用qiankun踩坑小计

a.主应用和子应用类名尽量不要一样,否则可能会导致样式错乱

b.解决开发环境“Cannot GET /hash.hot-update.json”问题  解决方案:链接

c.开发环境子组件使用nginx代理导致静态资源一直请求(暂时不支持)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A-wliang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值