美文网首页
qiankun 微前端应用实践与部署

qiankun 微前端应用实践与部署

作者: 我爱盐酥鸡 | 来源:发表于2020-09-14 20:40 被阅读0次

    微前端应用分为主应用与子应用,部署方式是分别编译好主应用与子应用,将主应用与子应用部署到 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' : ''
          }
        }
      ]
    }
    

    相关文章

      网友评论

          本文标题:qiankun 微前端应用实践与部署

          本文链接:https://www.haomeiwen.com/subject/kbltyktx.html