关于前端:基于qiankun微前端实战部署粗略笔记

4次阅读

共计 13903 个字符,预计需要花费 35 分钟才能阅读完成。

🚀 基于 qiankun 微前端实战 + 部署粗略笔记

因业务须要,以下文字纯集体 qiankun 实战学习笔记,不谈原理只记操作过程,内容不免有纰漏局部,敬请不吝赐教批评指正。

✨ 指标场景

📝 准备知识点

  • 已对 qiankun 微前端有了初步意识;
  • 相熟 react、vue;
  • 理解 github、docker、jenkins、nginx;

🔧 技术栈

🍔 基座

  • 应用 create-react-app 初始化我的项目;
  • 装置"qiankun": "^2.4.0";
  • 代码地址:react-app-qiankun-main;
  • 独立仓库,独立部署,独立域名:https://qiankun.xiaoqiang.tech;

🍟 react 子利用

  • 应用 create-react-app 初始化我的项目;
  • 装置"react-app-rewired": "^2.1.8""react-router-dom": "^5.2.0";
  • 代码地址:react-app-qiankun-sub;
  • 独立仓库,独立部署,独立域名:https://react.xiaoqiang.tech;

🌮 vue 子利用

  • 应用 vue-cli 初始化我的项目,对应"vue": "^3.0.0";
  • 装置"vue-router": "^4.0.0-beta.11";
  • 代码地址:vue-cli-qiankun-sub;
  • 独立仓库,独立部署,独立域名:https://vue.xiaoqiang.tech;

🚴‍♂️ 疾速上手

🍔 基座

  • 1. 初始化我的项目

    npm init react-app react-app-qiankun-main
  • 2. 装置qiankun

    yarn add qiankun # 或者 npm i qiankun -S
  • 3. 目录构造
react-app-qiankun-main
├── .env.local             // 本地环境
├── .env.development.local // 测试环境
├── .env.production.local  // 生产环境
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── components
    │     └── Loading.jsx
    ├── store
    │     └── store.js    // 主利用的全局状态
    ├── apps.js           // 子利用配置
    ├── App.css
    ├── App.js            // 基座布局,挂载子利用
    ├── App.test.js
    ├── index.css
    ├── index.js          // 主利用中注册微利用
    ├── logo.svg
    ├── reportWebVitals.js
    └── setupTests.js

🍔 基座(开撸代码)

  • 新增 3 个.env 文件,次要配置不同环境的对应的域名

    • .env/.env.development.local(此处暂未辨别本地和测试的域名,所有环境变量值都保持一致)
      REACT_APP_SUB_REACT=//localhost:2233/react
      REACT_APP_SUB_VUE=//localhost:3344/vue
      PORT=1122
    • .env.production.local(生产环境)
      REACT_APP_SUB_REACT = https://react.xiaoqiang.tech
      REACT_APP_SUB_VUE = https://vue.xiaoqiang.tech
  • 批改 index.html 挂载 dom 的默认 id,避免与子利用 id 抵触

      // 默认 root => main-root
      <div id="main-root"></div>
  • 新增 store/store.js,配置主利用的全局状态

      import {initGlobalState} from 'qiankun';
    
      const initialState = {
        user: {name: 'qiankun'}
      };
    
      const actions = initGlobalState(initialState);
    
      actions.onGlobalStateChange((state, prev) => {for(const key in state) {initialState[key] = state[key];
        }
      })
    
      // 非官方 api,https://github.com/umijs/qiankun/pull/729
      actions.getGlobalState = (key) => {return key ? initialState[key] : initialState;
      }
    
      export default actions;
  • 批改 src/App.js,次要实现基座页面布局及减少挂载子利用的 dom(id=”subapp-viewport”)

      function App(props) {
        // ... 省略,具体可见源码
        return (
          <>
            <div className="mainapp">
              {/* 标题栏 */}
              <header className="mainapp-header">
                <ul className="mainapp-header-sidemenu">
                  {/* 侧边栏 省略,具体可见源码 */}
                </ul>
              </header>
              <div className="mainapp-main">
                {/* 子利用 */}
                <main id="subapp-viewport"></main>
              </div>
            </div>
          </>
        );
      }
  • 减少 apps.js,子利用的配置

      import store from './store/store'
      const microApps = [
        {
          name: 'react',
          entry: process.env.REACT_APP_SUB_REACT,
          activeRule: '/react',
          container: '#subapp-viewport',
        },
        {
          name: 'vue',
          entry: process.env.REACT_APP_SUB_VUE,
          activeRule: '/vue',
          container: '#subapp-viewport',
        },
      ]
    
      const apps = microApps.map(item => {
        return {
          ...item,
          props: {
            routerBase: item.activeRule,
            getGlobalState: store.getGlobalState,
          }
        }
      })
    
      export default apps
  • 批改 src/index.js,主利用中注册微(子)利用

      import React from 'react';
      import ReactDOM from 'react-dom';
      import './index.css';
      import App from './App';
      import {registerMicroApps, start, setDefaultMountApp} from 'qiankun';
      import apps from './apps'
    
      function render({appContent, loading}) {const container = document.getElementById('main-root');
        ReactDOM.render(
          <React.StrictMode>
            <App loading={loading} content={appContent} />
          </React.StrictMode>,
          container,
        )
      }
    
      const loader = loading => render({loading});
    
      render({loading: true});
    
      const microApps = apps.map((app => ({
        ...app,
        loader,
      })))
      registerMicroApps(microApps, {
        beforeLoad: app => {console.log('before load app.name=====>>>>>', app.name)
        },
        beforeMount: [
          app => {console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name)
          }
        ],
        afterMount: [
          app => {console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name)
          }
        ],
        afterUnmount: [
          app => {console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name)
          }
        ]
      })
    
      setDefaultMountApp('/react')
    
      start();
  • 本地启动

      npm start

🍟 react 子利用

  • 1. 初始化我的项目

    npm init react-app react-app-qiankun-sub
  • 2. 装置react-app-rewiredreact-router-dom

    npm i react-app-rewired --save-dev
    npm i react-router-dom --save
  • 3. 目录构造
react-app-qiankun-sub
├── .env                 // 本地环境
├── config-overrides.js  // 笼罩 create-react-app 的 webpack 配置
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── components
    │     └── LibVersion.jsx
    ├── pages
    │     └── Home.jsx
    ├── public-path.js // __webpack_public_path__
    ├── App.css
    ├── App.js         // 子利用布局
    ├── App.test.js
    ├── index.css
    ├── index.js       // 子利用入口,挂载 dom 导出相应的生命周期钩子
    ├── logo.svg
    ├── reportWebVitals.js
    └── setupTests.js

🍟 react 子利用(开撸代码)

  • 新增 1 个.env 文件,次要配置本地环境

    此处 PORT 须要和基座 REACT_APP_SUB_REACT 端口保持一致

      PORT=2233
  • 批改 index.html 挂载 dom 的默认 id,避免与基座及其他子利用 id 抵触

      // 默认 root => sub-react-root
      <div id="sub-react-root"></div>
  • 新增 src/public-path.js,__webpack_public_path__

      if (window.__POWERED_BY_QIANKUN__) {
        // eslint-disable-next-line no-undef
        __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
      }
  • 批改 src/App.js,次要实现子利用页面布局(略,见源码)
  • 批改 src/index.js,微(子)利用导出相应的生命周期钩子

      import './public-path';
      import React from 'react';
      import ReactDOM from 'react-dom';
      import './index.css';
      import App from './App';
    
      function getSubRootContainer(container) {return container ? container.querySelector('#sub-react-root') : document.querySelector('#sub-react-root');
      }
    
      function render(props) {const { container} = props;
        ReactDOM.render(
          <React.StrictMode>
            <App store={{...props}} />
          </React.StrictMode>,
          getSubRootContainer(container),
        );
      }
    
      function storeTest(props) {props.onGlobalStateChange((value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), true);
        props.setGlobalState({
          ignore: props.name,
          user: {name: props.name,},
        });
      }
    
      if (!window.__POWERED_BY_QIANKUN__) {render({});
      }
    
      export async function bootstrap() {console.log('react app bootstraped');
      }
    
      export async function mount(props) {console.log('props from main framework', props);
        storeTest(props);
        render(props);
      }
    
      export async function unmount(props) {const { container} = props;
        ReactDOM.unmountComponentAtNode(getSubRootContainer(container));
      }
  • 减少 config-overrides.js,笼罩 create-react-app 的 webpack 配置

      const {name} = require('./package');
      module.exports = {
        webpack: config => {config.output.library = `${name}-[name]`;
          config.output.libraryTarget = 'umd';
          config.output.jsonpFunction = `webpackJsonp_${name}`;
          return config;
        },
        devServer: (configFunction) => {return (proxy, allowedHost) => {const config = configFunction(proxy, allowedHost);
            config.historyApiFallback = true;
            config.open = false;
            config.hot = false;
            config.watchContentBase = false;
            config.liveReload = false;
            config.headers = {'Access-Control-Allow-Origin': '*',};
            return config;
          }
        }
      }
  • 批改 package.json

      "scripts": {
        -   "start": "react-scripts start",
        +   "start": "react-app-rewired start",
        -   "build": "react-scripts build",
        +   "build": "react-app-rewired build",
        -   "test": "react-scripts test",
        +   "test": "react-app-rewired test",
        "eject": "react-scripts eject"
      },
  • 本地启动

      npm start

🌮 vue 子利用

  • 1. 初始化我的项目

    npm install -g @vue/cli-service-global
    vue create vue-cli-qiankun-sub
  • 2. 装置vue-router

    npm i vue-router --save
  • 3. 目录构造
vue-cli-qiankun-sub
├── .env                 // 本地环境
├── vue.config.js        // vue 可选的配置文件
├── babel.config.js
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── components
    │    └── HelloWorld.vue
    ├── router
    │     └── index.js
    ├── views
    │     └── Home.vue
    ├── public-path.js  // __webpack_public_path__
    ├── App.vue         // 子利用布局
    └── main.js         // 子利用入口,挂载 dom 导出相应的生命周期钩子

🌮 vue 子利用(开撸代码)

  • 新增 1 个.env 文件,次要配置本地环境

    此处 PORT 须要和基座 REACT_APP_SUB_VUE 端口保持一致

      PORT=3344
  • 批改 index.html 挂载 dom 的默认 id,避免与基座及其他子利用 id 抵触

      // 默认 root => sub-vue-root
      <div id="sub-vue-root"></div>
  • 新增 src/public-path.js,__webpack_public_path__

      if (window.__POWERED_BY_QIANKUN__) {
        // eslint-disable-next-line no-undef
        __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
      }
  • 批改 src/App.vue,次要实现子利用页面布局(略,见源码)
  • 批改 src/mian.js,微(子)利用导出相应的生命周期钩子

      import './public-path';
      import {createApp} from 'vue';
      import {createRouter, createWebHistory} from 'vue-router';
      import App from './App.vue';
      import routes from './router';
      import store from './store';
    
      let router = null;
      let instance = null;
      let history = null;
    
    
      function render(props = {}) {const { container} = props;
        history = createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue' : '/');
        router = createRouter({
          history,
          routes,
        });
    
        instance = createApp(App);
        instance.use(router);
        instance.use(store);
        instance.mount(container ? container.querySelector('#sub-vue-root') : '#sub-vue-root');
      }
    
      if (!window.__POWERED_BY_QIANKUN__) {render();
      }
    
      export async function bootstrap() {console.log('%c', 'color: green;', 'vue3.0 app bootstraped');
      }
    
      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 mount(props) {storeTest(props);
        render(props);
        instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange;
        instance.config.globalProperties.$setGlobalState = props.setGlobalState;
      }
    
      export async function unmount() {instance.unmount();
        instance._container.innerHTML = '';
        instance = null;
        router = null;
        history.destroy();}
  • 减少 vue.config.js 配置文件

      const path = require('path');
      const {name} = require('./package');
    
      function resolve(dir) {return path.join(__dirname, dir);
      }
    
      const port = process.env.PORT;
    
      module.exports = {
        outputDir: 'dist',
        assetsDir: 'static',
        filenameHashing: true,
        devServer: {
          hot: true,
          disableHostCheck: true,
          port,
          overlay: {
            warnings: false,
            errors: true,
          },
          headers: {'Access-Control-Allow-Origin': '*',},
        },
        // 自定义 webpack 配置
        configureWebpack: {
          resolve: {
            alias: {'@': resolve('src'),
            },
          },
          output: {
            // 把子利用打包成 umd 库格局
            library: `${name}-[name]`,
            libraryTarget: 'umd',
            jsonpFunction: `webpackJsonp_${name}`,
          },
        },
      };
    
  • 批改 package.json

      "scripts": {+   "start": "vue-cli-service serve",},
  • 本地启动

      npm start

🏄 预览

以上操作完后,能够间接通过基座预览,子利用也可独立预览

基座预览

http://localhost:1122/

react 子利用预览

http://localhost:2233/

vue 子利用预览

http://localhost:3344/

🔨 部署

备选计划

  • 1. 单域名部署;
// 基座:https://qiankun.xiaoqiang.tech
// react 子利用:https://qiankun.xiaoqiang.tech/react
// vue 子利用:https://qiankun.xiaoqiang.tech/vue
// 编译后服务器存储目录
react-app-qiankun
├── main
│   └── index.html
├── react
│   └── index.html
└── vue
    └── index.html
  • 2. 多域名独立部署;(当篇笔记抉择了多域名部署)
// 基座:https://qiankun.xiaoqiang.tech
// 编译后服务器我的项目独立存储目录
react-app-qiankun-main
  └── index.html
// react 子利用:https://react.xiaoqiang.tech
// 编译后服务器我的项目独立存储目录
react-app-qiankun-sub
  └── index.html
// vue 子利用:https://vue.xiaoqiang.tech
// 编译后服务器我的项目独立存储目录
vue-cli-qiankun-sub
└── index.html

部署(以下只初略记录部署过程,过于简陋)

  • 前提:已购云服务器,并已装置 docker、nginx、jenkins、3 个独立域名及 ssl 证书
  • 本地编码,github 存储代码,别离新建 3 个公开代码库
// 基座:react-app-qiankun-main 存储到 https://github.com/niexq/react-app-qiankun-main
// react 子利用:react-app-qiankun-sub 存储到 https://github.com/niexq/react-app-qiankun-sub
// vue 子利用:vue-cli-qiankun-sub 存储到 https://github.com/niexq/vue-cli-qiankun-sub
  • github,jenkins 继续集成
// 具体配置步骤略
// github webHooks 设置
// jenkins 构建局部执行 shell
BUILD_ID=dontKillMe
cd /var/jenkins_home/workspace/react-app-qiankun-main
npm install
npm run build
rm -rf /srv/www/react-app-qiankun-main
cp -rf /var/jenkins_home/workspace/react-app-qiankun-main/build /srv/www/react-app-qiankun-main/
  • 应用 nginx 代理
    nginx.conf

user root;
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 443;
        server_name qiankun.xiaoqiang.tech; # 你的域名
        ssl on;
        root www/react-app-qiankun-main; # 前台文件寄存文件夹,可改成别的
        index index.html index.htm; # 下面配置的文件夹外面的 index.html
        ssl_certificate cert/5543142_qiankun.xiaoqiang.tech.pem;   #将 domain name.pem 替换成您证书的文件名。ssl_certificate_key cert/5543142_qiankun.xiaoqiang.tech.key;   #将 domain name.key 替换成您证书的密钥文件名。ssl_session_timeout 5m;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        location / {
          # 用于配合 browserHistory 应用
          try_files $uri $uri/ /index.html;
          # root /srv/www/react-app-qiankun-main;
          # index index.html index.htm;
        }
    }

    server {
        listen 443;
        server_name react.xiaoqiang.tech; # 你的域名
        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';
        if ($request_method = 'OPTIONS') {return 204;}
        ssl on;
        root www/react-app-qiankun-sub; # 前台文件寄存文件夹,可改成别的
        index index.html index.htm; # 下面配置的文件夹外面的 index.html
        ssl_certificate cert/4325684_react.xiaoqiang.tech.pem;   #将 domain name.pem 替换成您证书的文件名。ssl_certificate_key cert/4325684_react.xiaoqiang.tech.key;   #将 domain name.key 替换成您证书的密钥文件名。ssl_session_timeout 5m;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        location / {
          # 用于配合 browserHistory 应用
          try_files $uri $uri/ /index.html;
          # root /srv/www/react-app-qiankun-sub;
          # index index.html index.htm;
        }
    }

    server {
        listen 443;
        server_name vue.xiaoqiang.tech; # 你的域名
        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';
        if ($request_method = 'OPTIONS') {return 204;}
        ssl on;
        root www/vue-cli-qiankun-sub; # 前台文件寄存文件夹,可改成别的
        index index.html index.htm; # 下面配置的文件夹外面的 index.html
        ssl_certificate cert/5556275_vue.xiaoqiang.tech.pem;   #将 domain name.pem 替换成您证书的文件名。ssl_certificate_key cert/5556275_vue.xiaoqiang.tech.key;   #将 domain name.key 替换成您证书的密钥文件名。ssl_session_timeout 5m;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        location / {
          # 用于配合 browserHistory 应用
          try_files $uri $uri/ /index.html;
          # root /srv/www/vue-cli-qiankun-sub;
          # index index.html index.htm;
        }
    }
}

docker 运行 nginx 命令,重点关注-v 挂载目录

docker run --name nginx -p 80:80 -p 443:443 -v /root/nginx/config/nginx.conf:/etc/nginx/nginx.conf -v /root/nginx/cert:/etc/nginx/cert -v /root/nginx/logs:/var/log/nginx -v /srv/www/react-app-qiankun-main:/etc/nginx/www/react-app-qiankun-main -v /srv/www/react-app-qiankun-sub:/etc/nginx/www/react-app-qiankun-sub -v /srv/www/vue-cli-qiankun-sub:/etc/nginx/www/vue-cli-qiankun-sub --restart=always -d nginx:stable

🌴 总结

没有总结,遇到的问题太多,笔记总结的太杂,前期再整顿分享

线上预览地址:https://qiankun.xiaoqiang.tech

子利用线上也可独立预览

react 子利用预览:https://react.xiaoqiang.tech

vue 子利用预览:https://vue.xiaoqiang.tech

源码地址:https://github.com/niexq/reac…

🧩 参考链接

qiankun

qiankun-example

qiankun 微前端计划实际及总结

💯 跳过上述繁琐步骤是否可行

智慧的抉择

🏆 写在最初

能保持到最初的都是壮士,感激浏览,欢送 star 激励

正文完
 0