Implementation of automatic deployment of micro front-end qiankun+docker+nginx with gitlab-ci/cd

Dachang Technology Advanced Front-end Node Advanced

Click on the top programmer's growth guide, pay attention to the public number

Reply 1, join the advanced Node exchange group

Original link: <https://juejin.cn/post/6981339862901194759?
Author: Rainbow on Paper

Introduction to the Technology Stack

  • micro frontend

  • qiankun

  • docker

  • gitlab-ci/cd

  • nginx

If you don't understand the article after reading the article, you can cooperate with [video explanation to view this article] Video address: https://www.bilibili.com/video/BV1Qg411u7C9

What is a micro frontend

Micro front-end is a technical means and method strategy for multiple teams to jointly build modern web applications by independently publishing functions.

The micro-frontend architecture has the following core values:

  • Technology stack independent The main framework does not restrict access to the application technology stack, and micro-applications have full autonomy

  • Independent development and independent deployment The micro-application warehouse is independent, and the front and back ends can be developed independently. After the deployment is completed, the main framework automatically completes the synchronization update

  • Incremental upgrade

    In the face of various complex scenarios, it is usually difficult for us to upgrade or refactor an existing system in full, and micro-frontend is a very good means and strategy for implementing incremental refactoring.

  • When running independently, the state between each micro-application is isolated, and the runtime state is not shared

what is qiankun

qiankun is a production-ready micro-frontend framework. It is based on single-spa and has the capabilities required by micro-frontend systems such as js sandbox, style isolation, HTML Loader, and preloading. qiankun can be used in any js framework, and micro-app access is as simple as embedding an iframe system.

qiankun's core design philosophy

Reference address: qiankun.umijs.org/zh/guide

  • Simple

    Since the main application micro-application can be independent of the technology stack, qiankun is just a jQuery-like library for users. You need to call several qiankun APIs to complete the micro-frontend transformation of the application. At the same time, due to the design of qiankun's HTML entry and sandbox, the access of micro-applications is as simple as using iframe.

  • Decoupling/tech stack agnostic

    The core goal of the micro-frontend is to disassemble the monolithic application into several loosely coupled micro-applications that can be autonomous, and many designs of qiankun adhere to this principle, such as HTML entry, sandbox, and inter-application communication. Only in this way can micro-applications truly have the ability to develop and run independently.

Why not use Iframe

Reference address: www.yuque.com/kuitos/gky7…

If you don't consider experience issues, iframes are almost the perfect micro-frontend solution.

The biggest feature of iframe is to provide the browser's native hard isolation solution, whether it is style isolation, js isolation and other problems can be perfectly solved. But his biggest problem is that his isolation cannot be broken, resulting in the inability to share the context between applications, and the resulting development experience and product experience.

  1. The url is out of sync. When the browser refreshes the iframe url, the state is lost, and the back and forward buttons cannot be used.

  2. UI is not synchronized, DOM structure is not shared. Imagine a pop-up box with a mask layer in the iframe in the lower right corner of the screen. At the same time, we require the pop-up box to be displayed in the center of the browser and automatically center when the browser is resized.

  3. The global context is completely isolated, and memory variables are not shared. For the communication and data synchronization requirements of the internal and external systems of the iframe, the cookie of the main application should be transparently transmitted to the sub-applications with different root domain names to achieve the free login effect.

  4. slow. Each entry of a sub-application is a process of rebuilding the browser context and reloading resources.

Some of the problems are easy to solve (Problem 1), some problems we can turn a blind eye to (Problem 4), but some problems are difficult for us to solve (Problem 3) or even impossible to solve (Problem 2) , and these unsolvable problems will bring very serious experience problems to the product, which eventually led us to abandon the iframe solution.

The core value of micro frontend

www.yuque.com/kuitos/gky7…

project idea

Before talking about the specific technical implementation, let's take a look at what we want to achieve.

Micro front-end schematic

e61fa510905d574515a548563e0b95f7.png

Sub-apps are dynamically loaded based on clicks on the main app's navigation

Deployment logic

There are many ideas for deployment, I will talk about the ways I have tried here:

  • Only use one nginx container, deploy multiple applications by listening to different ports, and then add the corresponding routing proxy to the sub-application in the port of the main application

    This method is the simplest but not suitable for automated deployment of gitlab-ci/cd, so I just initially tested the implementation of nginx deployment micro-frontend

  • Use multiple nginx containers, each container exposes a port, and then add the corresponding routing proxy to the sub-application through the main application

    This method can be implemented, but it will expose multiple ports on the server, the security will be reduced, and the external can also directly access the sub-application through the port

  • Use multiple nginx containers, only expose the port of the main application, the main application connects to the sub-application, and then accesses through the nginx proxy

    This method is the most ideal, only one port needs to be exposed, all agents are between the containers, and they are insensitive to the outside world. The following is an illustration of the implementation.

7e6256a8828c2f157c153d7397ee626f.png

qiankun

install qiankun

$ yarn add qiankun # 或者 npm i qiankun -S

Register the microapp in the main app

import { registerMicroApps, addGlobalUncaughtErrorHandler, start } from 'qiankun';

const apps = [
  {
    name: 'ManageMicroApp',
    entry: '/system/', // 本地开发的时候使用 //localhost:子应用端口
    container: '#frame',
    activeRule: '/manage',
  },
]

/**
 * 注册微应用
 * 第一个参数 - 微应用的注册信息
 * 第二个参数 - 全局生命周期钩子
 */
registerMicroApps(apps,{
  // qiankun 生命周期钩子 - 微应用加载前
  beforeLoad: (app: any) => {
    console.log("before load", app.name);
    return Promise.resolve();
  },
  // qiankun 生命周期钩子 - 微应用挂载后
  afterMount: (app: any) => {
    console.log("after mount", app.name);
    return Promise.resolve();
  },
});

/**
 * 添加全局的未捕获异常处理器
 */
addGlobalUncaughtErrorHandler((event: Event | string) => {
  console.error(event);
  const { message: msg } = event as any;
  // 加载失败时提示
  if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
    console.error("微应用加载失败,请检查应用是否可运行");
  }
});

start();

After the micro-application information is registered, once the url of the browser changes, the matching logic of qiankun will be automatically triggered, and all the micro-applications matched by the activeRule rule will be inserted into the specified container, and the micro-applications will be called in turn to expose the lifecycle hooks.

If the micro application is not directly associated with the route, you can also choose to manually load the micro application:

import { loadMicroApp } from 'qiankun';


loadMicroApp({
  name: 'app',
  entry: '//localhost:7100',
  container: '#yourContainer',
});

Micro application

Micro-apps can access the qiankun main application without installing any other dependencies.

1. Export the corresponding lifecycle hooks

bootstrapMicro applications need to export , mount, and unmountthree lifecycle hooks in their own entry js (usually the entry js of your configured webpack) for the main application to call at the appropriate time.

import Vue from 'vue';
import VueRouter from 'vue-router';

import './public-path';
import App from './App.vue';
import routes from './routes';
import SharedModule from '@/shared'; 

Vue.config.productionTip = false;

let instance = null;
let router = null;
// 如果子应用独立运行则直接执行render
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
 * 渲染函数
 * 主应用生命周期钩子中运行/子应用单独启动时运行
 */
function render(props = {}) {
  // SharedModule用于主应用于子应用的通讯
  // 当传入的 shared 为空时,使用子应用自身的 shared
  // 当传入的 shared 不为空时,主应用传入的 shared 将会重载子应用的 shared
  const { shared = SharedModule.getShared() } = props;
  SharedModule.overloadShared(shared);

  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/manage/' : '/',
    mode: 'history',
    routes
  });

  // 挂载应用
  instance = new Vue({
    router,
    render: (h) => h(App)
  }).$mount('#app');
}

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log('vue app bootstraped');
}
/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log('vue mount', props);
  render(props);
}
/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log('vue unmount');
  instance.$destroy();
  instance = null;
  router = null;
}
/**
 * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
 */
export async function update(props) {
  console.log('update props', props);
}

The above code also references a public-pathfile:

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

This mainly solves the problem of incorrect addresses of scripts, styles, pictures, etc. dynamically loaded by micro-applications.

2. Configure the packaging tool for the micro-app

In addition to exposing the corresponding lifecycle hooks in the code, in order for the main application to correctly identify some of the information exposed by the micro-application, the micro-application packaging tool needs to add the following configuration:

webpack:

const packageName = require('./package.json').name;


module.exports = {
  publicPath: '/system/', //这里打包地址都要基于主应用的中注册的entry值
  output: {
    library: 'ManageMicroApp', // 库名,与主应用注册的微应用的name一致
    libraryTarget: 'umd', // 这个选项会尝试把库暴露给前使用的模块定义系统,这使其和CommonJS、AMD兼容或者暴露为全局变量。
    jsonpFunction: `webpackJsonp_${packageName}`,
  },
};

Summary of key points

  • Configuration when the main application is registered

    const apps = [
      {
        name: 'ManageMicroApp',
        entry: '/system/',  // http://localhost/system/ 这里会通过nginx代理指向对应的子应用地址
        container: '#frame',
        activeRule: '/manage',
      },
    ]

    When the main application registers the micro application, it entrycan be a relative path, activeRulenot the entrysame as (otherwise the main application page refreshes and it becomes a micro application)

  • base of vue routing

    router = new VueRouter({
      base: window.__POWERED_BY_QIANKUN__ ? '/manage/' : '/',
      mode: 'history',
      routes
    });

    If it is called by the main application, the base of the route is/manage/

  • webpack packaging configuration

    module.exports = {
      publicPath: '/system/',
    };

    For the webpackbuilt micro application, the webpackpackage of the micro application publicPathneeds to be configured as /system/, otherwise the micro application index.htmlcan be requested correctly, but index.htmlthe js/csspath micro application will not be brought /system/.

At this point, we have completed the configuration of the micro-frontend, and the next step is the configuration of nginx.

Production environment Nginx configuration

First hang up the nginx configuration of the main application

server {
        listen       80;
        listen       [::]:80 default_server;
        server_name  localhost;
        root         /usr/share/nginx/html;

        location / {
            try_files $uri $uri/ /index.html;
            index index.html;
        }
				# 前面我们配置的子应用entry是/system/,所以会触发这里的代理,代理到对应的子应用
        location /system/ {
    				 # -e表示只要filename存在,则为真,不管filename是什么类型,当然这里加了!就取反
             if (!-e $request_filename) {
                proxy_pass http://192.168.1.2; # 这里的ip是子应用docker容器的ip
             }
    				 # -f filename 如果 filename为常规文件,则为真
             if (!-f $request_filename) {
                proxy_pass http://192.168.1.2;
             }
             # docker运行的nginx不识别localhost的 所以这种写法会报502
             # proxy_pass  http://localhost:10200/;
             proxy_set_header Host $host;
         }

        location /api/ {
            proxy_pass http://后台地址IP/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }

Take a look at the sub-app

server {
    listen       80;
    listen       [::]:80 default_server;
    server_name  _2;
    root         /usr/share/nginx/html;

    # 这里必须加上允许跨域,否则主应用无法访问
    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 / {
        try_files $uri $uri/ /index.html;
        index index.html;
    }

    location /api/ {
        proxy_pass http://后台地址IP/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header REMOTE-HOST $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
}

dockerfile configuration

Here's a look at the sub-application

# 直接使用nginx镜像
FROM nginx
# 把上面配置的conf文件替换一下默认的
COPY nginx.conf /etc/nginx/nginx.conf
# nginx默认目录下需要能看见index.html文件
COPY dist/index.html /usr/share/nginx/html/index.html
# 再回头看一下部署逻辑图和qiankun注意点,必须要把所有的资源文件放到system文件下index.html才能正确加载
COPY dist /usr/share/nginx/html/system

Look at the main application again

# 这里主应用没有直接使用nginx,因为nginx反向代理的/api/会出现404的问题,原因未知!
FROM centos
# 安装nginx
RUN yum install -y nginx
# 跳转到/etc/nginx
WORKDIR /etc/nginx
# 替换配置文件
COPY nginx.conf nginx.conf
# 跳转到/usr/share/nginx/html
WORKDIR /usr/share/nginx/html
# 主应用正常打包,所以直接把包放进去就行
COPY dist .
# 暴露80端口
EXPOSE 80
# 运行nginx
CMD nginx -g "daemon off;"

gitlab-ci/cd configuration

Let's take a look at the sub-application first, and only talk about the key points

image: node

stages:
  - install
  - build
  - deploy
  - clear

cache:
  key: modules-cache
  paths:
    - node_modules
    - dist

安装环境:
  stage: install
  tags:
    - vue
  script:
    - npm install yarn
    - yarn install

打包项目:
  stage: build
  tags:
    - vue
  script:
    - yarn build

部署项目:
  stage: deploy
  image: docker
  tags:
    - vue
  script:
  	# 通过dockerfile构建项目的镜像
    - docker build -t rainbow-system .
    # 如果存在之前创建的容器先删除
    - if [ $(docker ps -aq --filter name=rainbow-admin-system) ];then docker rm -f rainbow-admin-system;fi
    # 通过刚刚的镜像创建一个容器 给容器指定一个网卡rainbow-net,这个网卡是我们自定义,创建方式后面会说,然后给定一个ip
    - docker run -d --net rainbow-net --ip 192.168.1.2 --name rainbow-admin-system rainbow-system

清理docker:
  stage: clear
  image: docker
  tags:
    - vue
  script:
    - if [ $(docker ps -aq | grep "Exited" | awk '{print $1 }') ]; then docker stop $(docker ps -a | grep "Exited" | awk '{print $1 }');fi
    - if [ $(docker ps -aq | grep "Exited" | awk '{print $1 }') ]; then docker rm $(docker ps -a | grep "Exited" | awk '{print $1 }');fi
    - if [ $(docker images | grep "none" | awk '{print $3}') ]; then docker rmi $(docker images | grep "none" | awk '{print $3}');fi

Look at the main application again, omit the repetition, and focus directly on the key points

部署项目:
  stage: deploy
  image: docker
  tags:
    - vue3
  script:
    - docker build -t rainbow-admin .
    - if [ $(docker ps -aq --filter name=rainbow-admin-main) ];then docker rm -f rainbow-admin-main;fi
    # 给容器指定一个网卡rainbow-net,然后给定一个ip,然后通过--link与之前创建的子应用连通,重点!
    - docker run -d -p 80:80 --net rainbow-net --ip 192.168.1.1 --link 192.168.1.2 --name rainbow-admin-main rainbow-admin

The above mentioned docker's custom network card, the generated command is as follows:

$ docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 rainbow-net

Summarize

So far, we have realized the automatic deployment of qiankun+docker and gitlab-ci/cd. We encountered many pits, and then came up with a relatively reasonable solution. If you have any questions, welcome to discuss.

Node 社群


我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:

1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长

点赞和在看就是最大的支持❤️

Guess you like

Origin blog.csdn.net/xgangzai/article/details/123564378