Skip to content

sunweijieMJ/monorepo-microapp

Repository files navigation

Monorepo Microapp

基于 monorepo 架构搭建的微前端模版工程

说明文档

目录结构

├── .husky
├── .vscode
|
├──── packages 源码目录
| ├── main-react
| ├── main-vue
| ├── micro-react
| ├── micro-vue
|
├──── scripts shell 脚本
├──── typings 类型文件
|
|-- .browserslistrc
|-- .cz-config.js
|-- .editorconfig
|-- .eslintignore
|-- .eslintrc.js
|-- .gitignore
|-- .markdownlint.json
|-- .npmrc
|-- .prettierignore
|-- .prettierrc.js
|-- .stylelintignore
|-- .stylelint.js
|-- CHANGELOG.md
|-- commitlint.config.js
|-- cspell.config.js
|-- package.json
|-- pnpm-lock.yaml
|-- pnpm-workspace.yaml
|-- README.md
|-- tsconfig.json

开发环境

node pnpm

该项目基于 monorepo 的架构,pnpm 安装依赖,typescript 编写代码。

  • 使用 eslintstylelint 校验代码,prettier 格式化代码,i18n Ally 翻译多语言。需要安装相关的 vscode 插件

  • 全局安装 pnpm

    npm i pnpm -g

微前端要点

  • 构建主应用
  • 构建子应用
  • 应用间通信
  • 资源共享
  • 内存溢出
  • 打包部署

构建主应用

  • 需要一个渲染子应用的 div 容器
  • 初始化的时候手动加载子应用
import classnames from 'classnames';
import _ from 'lodash';
import {
  addGlobalUncaughtErrorHandler, // 添加全局未捕获异常处理器
  loadMicroApp, // 手动加载一个微应用
  prefetchApps, // 预加载子应用
  registerMicroApps, // 注册子应用方法
  runAfterFirstMounted, // 第一个微应用 mount
  setDefaultMountApp, // 设默认启用的子应用
  start,
} from 'qiankun';
import React, { useEffect } from 'react';
import type { MenuList } from '../../config/menuList';
import menuList from '../../config/menuList';
import microApps from '../../config/microApps';
import LayoutAside from '../LayoutAside';
import LayoutHeader from '../LayoutHeader';
import LayoutNav from '../LayoutNav';
import './index.scss';

const microAppList = microApps;
// 当前激活应用名称
let activeMicroAppName = '';
// 子应用上限
const microAppLimit = 10;

// 获取当前激活的子应用
const getActiveMicroApp = (
  defaultPath = `/${window.location.pathname.split('/')[1]}`
) => {
  const activeMicroApp = _.find(microAppList, (item) =>
    Array.isArray(item.activeRule)
      ? item.activeRule.includes(defaultPath)
      : item.activeRule === defaultPath
  );

  return activeMicroApp;
};

// 手动加载子应用
const manualLoadMicroApps = (defaultPath?: string, singular = false) => {
  const microApp = getActiveMicroApp(defaultPath);
  if (window.activeMicroApp?.name === microApp?.name || !microApp) return;

  // 单实例模式
  if (singular) {
    // 卸载前一个子应用
    if (window.activeMicroApp?.getStatus() === 'MOUNTED') {
      window.activeMicroApp.unmount();
      window.activeMicroApp = null;
      window.activatedMicroApp = [];
      activeMicroAppName = '';
    }

    // 加载新的子应用
    const newMicroApp = {
      name: microApp.name,
      ...loadMicroApp(microApp, { singular: true }),
    };
    newMicroApp.mountPromise.then(() => {
      activeMicroAppName = microApp.name;
    });

    window.activeMicroApp = newMicroApp;
    window.activatedMicroApp = [window.activeMicroApp];
  } else {
    const activeMicroApp = _.find(
      window.activatedMicroApp,
      (item) => item.name === microApp.name
    );

    // 判断当前子应用是否加载过
    if (activeMicroApp) {
      window.activeMicroApp = activeMicroApp;
      activeMicroApp.mountPromise.then(() => {
        activeMicroAppName = microApp.name;
      });
    } else {
      const newMicroApp = {
        name: microApp.name,
        ...loadMicroApp(microApp),
      };
      newMicroApp.mountPromise.then(() => {
        activeMicroAppName = microApp.name;
      });

      window.activeMicroApp = newMicroApp;
      window.activatedMicroApp.push(window.activeMicroApp);
      // 超出数量限制,卸载第一个
      if (window.activatedMicroApp.length > microAppLimit) {
        window.activatedMicroApp.shift()?.unmount();
      }
    }
  }
};

// 自动加载子应用
const autoLoadMicroApps = (menuList: MenuList[]) => {
  let defaultPath = menuList[0].routePath;

  // 预加载子应用
  prefetchApps(microAppList);

  // 注册子应用
  registerMicroApps(microAppList);

  // 设置默认子应用
  const activePath = window.location.pathname.split('/')[1];
  if (activePath) {
    defaultPath = `/${activePath}`;
  }
  setDefaultMountApp(defaultPath);

  // 启动微服务
  start({
    prefetch: true,
  });

  // 第一个微应用 mount 后需要调用的方法
  runAfterFirstMounted(() => console.log('runAfterFirstMounted'));

  // 设置全局未捕获异常处理器
  addGlobalUncaughtErrorHandler((event) => console.log(event));
};

const Layout: React.FC = () => {
  useEffect(() => {
    // 设置默认子应用
    let defaultPath = '';
    let pathname = window.location.pathname;

    if (pathname.split('/')[1]) {
      defaultPath = `/${pathname.split('/')[1]}`;
    }
    if (!defaultPath && menuList.length) defaultPath = menuList[0].routePath;

    // 重置子应用数组
    if (!window.activatedMicroApp?.length) window.activatedMicroApp = [];

    manualLoadMicroApps(defaultPath);
  }, []);

  return (
    <div className="layout">
      <LayoutHeader></LayoutHeader>
      <LayoutAside></LayoutAside>
      <LayoutNav></LayoutNav>
      {microAppList.map((item, index) => {
        return (
          <div
            id={item.name}
            key={index}
            className={classnames('micro-app', {
              'active-app': activeMicroAppName === item.name,
            })}
          ></div>
        );
      })}
    </div>
  );
};

export default Layout;

构建子应用

  • 使用 create-react-app 构建一个 react 子应用工程
  • 改造 index.tsx 入口文件
import React from 'react';
import ReactDOM from 'react-dom/client';
import Router from './router';
import './public-path';

// eslint-disable-next-line no-underscore-dangle
const POWERED_BY_QIANKUN = window.__POWERED_BY_QIANKUN__;

const root = ReactDOM.createRoot(
  document.querySelector('#micro-react-root') as HTMLElement
);

function render() {
  root.render(
    <React.StrictMode>
      <Router />
    </React.StrictMode>
  );
}

export async function bootstrap() {
  console.log('react bootstrap');
}

export async function mount() {
  console.log('react mount');
  render();
}

export async function update() {
  console.log('react update');
  render();
}

export async function unmount() {
  console.log('react unmount');
  root.unmount();
}

// 单独开发环境
if (!POWERED_BY_QIANKUN) mount();

子应用的 craco.config.js

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

const port = 3002;
const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  webpack: {
    configure: (webpackConfig) => {
      if (!isDev) {
        webpackConfig.output = {
          ...webpackConfig.output,
          // 微前端打包配置
          library: `${name}-[name]`,
          libraryTarget: `umd`,
        };
      }

      return webpackConfig;
    },
  },
  devServer: {
    https: false,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
};

应用间通信

qiankun 官方提供了 api 去解决这个问题。这里只做基本演示,也可以用其他中间件去处理应用通信的问题。例如 rxjsvuexredux

  • 主应用
import { initGlobalState, MicroAppStateActions } from 'qiankun';

// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
  • 子应用
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });

  props.setGlobalState(state);
}

资源共享

1.共享模块

  • 该项目采用的是 monorepo 架构来共享依赖
  • 也可采用 npm Webpack Externals Webpack DLL 等方式来共享模块

2.共享资源

  • props 传递
  • window 传递

内存溢出

qiankun 会将微应用的 JS/CSS 内容都记录在全局变量中,如果一直重复的挂载应用没有卸载,会导致内存占用过多,导致页面卡顿

  • 微应用卸载的时候清空微应用注册的附加内容及 DOM 元素
  • 设置自动销毁时间,去销毁那些长时间挂载的应用
  • 设置最大运行应用数量,超过规定的数量的时候吧第一个应用销毁

打包部署

以上做了这么多,能够部署到服务器上才算成功。我这里用的是 nginx

server {
  listen          3000;
  server_name     localhost;
  root            /Users/sunweijie/monorepo-microapp/packages/main-react/dist/;

  location / {
    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';

    index index.html index.htm;

    try_files $uri $uri/ /index.html;
  }
}

server {
  listen          3001;
  server_name     localhost;
  root            /Users/sunweijie/monorepo-microapp/packages/micro-vue/dist/;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

server {
  listen          3002;
  server_name     localhost;
  root            /Users/sunweijie/monorepo-microapp/packages/micro-react/dist/;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

常用脚本

  • 安装依赖

    pnpm i
  • 启动应用

    pnpm start
  • 格式化代码

    pnpm format
  • lint 校验代码

    pnpm lint
  • 生成 CHANGELOG.md

    pnpm changelog
  • commit 代码

    git cz

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published