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 注册子应用,并编写导航页显示跳转逻辑。
QianKun Example - 锐客网导航
  • 应用一
  • 应用二

主应用 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.ymlversion: '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.confusernginx; worker_processes1; error_log/var/log/nginx/error.log warn; pid/var/run/nginx.pid; events { worker_connections1024; }http { include/etc/nginx/mime.types; default_typeapplication/octet-stream; log_formatmain'$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.logmain; sendfileon; #tcp_nopushon; keepalive_timeout65; #gzipon; include /etc/nginx/conf.d/*.conf; server { listen8889; server_name 192.168.2.192; location / { root /app/micro/portal; index index.html; try_files $uri $uri/ /index.html; } }server { listen7100; 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 { listen7101; 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 元素 // 如 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 微前端应用实践与部署】路由需要根据 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' : '' } } ] }

    推荐阅读