Skip to content

jwchan1996/qiankun-micro-app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

qiankun 微前端应用实践与部署

qiankun 微前端应用实践与部署(一)
qiankun 微前端应用实践与部署(二)
qiankun 微前端应用实践与部署(三)
qiankun 微前端应用实践与部署(四)

微前端应用分为主应用与子应用,部署方式是分别编译好主应用与子应用,将主应用与子应用部署到 nginx 配置好的目录即可。

代码仓库 https://github.com/jwchan1996/qiankun-micro-app

分别进入 portalapp1app2 根目录,执行:

开发模式

# portal
yarn
yarn start
# app1、app2
npm install
npm run dev

生产模式

# portal
yarn build
# app1、app2
npm run build

主应用

主应用 js 文件引入 qiankun 注册子应用,并编写导航页显示跳转逻辑。

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

<head>
  <meta charset="UTF-8">
  <title>QianKun Example</title>
</head>

<body>
  <div class="mainapp">
    <!-- 标题栏 -->
    <header class="mainapp-header">
      <h1>导航</h1>
    </header>
    <div class="mainapp-main">
      <!-- 侧边栏 -->
      <ul class="mainapp-sidemenu">
        <li class="app1">应用一</li>
        <li class="app2">应用二</li>
      </ul>
      <!-- 子应用  -->
      <main id="subapp-container"></main>
    </div>
  </div>

  <script src="./index.js"></script>
</body>

</html>

主应用 js 入口文件:

import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun';
import './index.less';

/**
 * 主应用 **可以使用任意技术栈**
 * 以下分别是 React 和 Vue 的示例,可切换尝试
 */
import render from './render/ReactRender';
// import render from './render/VueRender';

/**
 * Step1 初始化应用(可选)
 */
render({ loading: true });

const loader = loading => render({ loading });

/**
 * Step2 注册子应用
 */
registerMicroApps(
  [
    {
      name: 'app1',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : '//localhost:7100',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app1',
    },
    {
      name: 'app2',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7101' : '//localhost:7101',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app2',
    }
  ],
  {
    beforeLoad: [
      app => {
        console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
      },
    ],
    beforeMount: [
      app => {
        console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
      },
    ],
    afterUnmount: [
      app => {
        console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
      },
    ],
  },
);

const { onGlobalStateChange, setGlobalState } = initGlobalState({
  user: 'qiankun',
});

onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev));

setGlobalState({
  ignore: 'master',
  user: {
    name: 'master',
  },
});

/**
 * Step3 设置默认进入的子应用
 */
// setDefaultMountApp('/app1');

/**
 * Step4 启动应用
 */
start();

runAfterFirstMounted(() => {
  console.log('----------------------------------')
  console.log(process.env.NODE_ENV)
  console.log('----------------------------------')
  console.log('[MainApp] first app mounted');
});

//浏览器地址入栈
function push(subapp) { history.pushState(null, subapp, subapp) }

//配合导航页显示逻辑
function initPortal(){
  //主应用跳转
  document.querySelector('.app1').onclick = () => {
    document.querySelector('.mainapp-sidemenu').style.visibility = 'hidden'
    push('/app1')
  }
  document.querySelector('.app2').onclick = () => {
    document.querySelector('.mainapp-sidemenu').style.visibility = 'hidden'
    push('/app2')
  }

  //回到导航页
  document.querySelector('.mainapp-header h1').onclick = () => {
    push('/')
  }

  if(location.pathname !== '/'){
    document.querySelector('.mainapp-sidemenu').style.visibility = 'hidden'
  }else{
    document.querySelector('.mainapp-sidemenu').style.visibility = 'visible'
  }
  if(location.pathname.indexOf('login') > -1){
    document.querySelector('.mainapp-header').style.display = 'block'
  }else{
    document.querySelector('.mainapp-header').style.display = 'none'
  }

  //监听浏览器前进回退
  window.addEventListener('popstate', () => { 
    if(location.pathname === '/'){
      document.querySelector('.mainapp-sidemenu').style.visibility = 'visible'
    }
    if(location.pathname.indexOf('login') > -1){
      document.querySelector('.mainapp-header').style.display = 'block'
    }else{
      document.querySelector('.mainapp-header').style.display = 'none'
    }
  }, false)
}

initPortal()

docker nginx 配置

此处 nginx 主要作用是用于端口目录转发,并配置主应用访问子应用的跨域问题。

使用 docker 配置部署 nginx

# docker-compose.yml

version: '3.1'
services:
  nginx:
    restart: always
    image: nginx
    container_name: nginx
    ports: 
      - 8888:80
      - 8889:8889
      - 7100:7100
      - 7101:7101
    volumes: 
      - /app/volumes/nginx/nginx.conf:/etc/nginx/nginx.conf
      - /app/volumes/nginx/html:/usr/share/nginx/html
      - /app/micro/portal:/app/micro/portal
      - /app/micro/app1:/app/micro/app1
      - /app/micro/app2:/app/micro/app2

将编译后的主应用以及子应用放到对应的数据卷挂载目录即可,如主应用 /app/micro/portal
同理,也需要将配置好的 nginx.conf 文件放到指定的数据卷挂载目录,使用 docker-compose up -d 启动即可。

nginx 端口目录转发配置:

# nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;

    server {
      listen	8889;
      server_name 192.168.2.192;
      
      location / {
        root /app/micro/portal;
        index index.html;
        
        try_files $uri $uri/ /index.html;
      }
    }

    server {
      listen	7100;
      server_name 192.168.2.192;
      
      # 配置跨域访问,此处是通配符,严格生产环境的话可以指定为主应用 192.168.2.192:8889
      add_header Access-Control-Allow-Origin *;
      add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
      add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
      
      location / {
        root /app/micro/app1;
        index index.html;
        
        try_files $uri $uri/ /index.html;
      }
    }

    server {
      listen	7101;
      server_name 192.168.2.192;
      
      # 配置跨域访问,此处是通配符,严格生产环境的话可以指定为主应用 192.168.2.192:8889
      add_header Access-Control-Allow-Origin *;
      add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
      add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
      
      location / {
        root /app/micro/app2;
        index index.html;
        
        try_files $uri $uri/ /index.html;
      }
    }
}

子应用适配框架

下面子应用以常规 vue 项目为例。

入口文件 main.js

在入口文件增加 qiankun 环境判断,判断当前是 qiankuan 环境的则将子应用引入到主应用框架内,然后在主框架内执行正常的 vue 元素挂载。

// 在所有代码的文件之前引入判断
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

import Vue from "vue";
import App from "./App";
import router from "./router";

let instance = null;

function render(props = {}) {
  // 此处 container 是主应用生成的用于装载子应用的 div 元素
  // 如 <div id="__qiankun_microapp_wrapper_for_app_1_1596504716562__" />
  const { container } = props;
  
  instance = new Vue({
    router,
    render: h => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

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

function storeTest(props) {
  props.onGlobalStateChange &&
    props.onGlobalStateChange(
      (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
      true,
    );
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name,
      },
    });
}

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

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

export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}

router 配置

路由需要根据 qiankun 环境配置 base 路径,以及设置路由的 history 模式。

// router/index.js
const router = new Router({
  // 此处 /app1 是子应用在主应用注册的 activeRule
  base: window.__POWERED_BY_QIANKUN__ ? '/app1' : '/',
  mode: 'history',
  routes: [
    {
        ……
        ……
    }
  ]
})

// portal/index.js
registerMicroApps(
  [
    {
      name: 'app1',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : '//localhost:7100',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app1',
    }
  ]
)

子应用打包

打包 umd 格式

output: {
    library: 'portal',
    libraryTarget: 'umd'
}

字体图标与 css 背景图片路径问题

默认情况下,在 css 引用的资源使用 url-loader 加载打包出来是相对路径的,所以会出现子应用的资源拼接到主应用的 domain 的情况,造成加载资源失败。

因为 element-ui 的字体图标是在 css 里面引入的,还有相关背景图片的引入也是在 css 里,所以需要配置 webpackurl-loader,生产模式情况下直接指定资源前缀。

module: {
  rules: [
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("img/[name].[hash:7].[ext]"),
        //这里 192.168.2.192:7100 是子应用部署地址
        publicPath: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : ''
      }
    },
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("fonts/[name].[hash:7].[ext]"),
        publicPath: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7101' : ''
      }
    }
  ]
}