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:7100' : ''
      }
    }
  ]
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350