vue3+qiankun+Typescript开发微前端项目


路由为hash模式

一、主应用配置

1、安装 qiankun
yarn add qiankun 或者 npm i qiankun -S
2、注册微应用并启动
import { registerMicroApps, start } from 'qiankun';

const isDev = process.env.NODE_ENV === "development";
const apps = [
  {
    name: 'FirstMicroApp',
    entry: isDev ? '//localhost:8082' : '/first-micro-app/',
    container: '#microAppContainer',
    activeRule: '#/exhibitionHall',
  },
];
registerMicroApps(apps, {
  beforeLoad: [
    // app => {
    //   console.log("before load", app);
    // }
  ],
  beforeMount: [
    // app => {
    //   console.log("before mount", app);
    // }
  ],
  afterUnmount: [
    // app => {
    //   console.log("after unMount", app);
    // }
  ],
});

// 启动 qiankun
start();

二、微应用配置

1、在 src 目录新增 public-path.js
/* eslint-disable */
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
2、入口文件 main.ts 修改
import './public-path';
import { createApp } from 'vue';
import Antd from 'ant-design-vue';
import App from './App.vue';
import router from './router';
import store from './store';
import 'ant-design-vue/dist/antd.css';

// createApp(App).use(store).use(router).mount('#app');

type Props = {
  container?: HTMLElement;
}

let app: any = null;

function render(props: Props = {}) {
  const { container } = props;
  app = createApp(App);
  app.use(router);
  app.use(store);
  app.use(Antd);
  app.mount(container ? container.querySelector('#app') : '#app');
}

// 独立运行时
// eslint-disable-next-line
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap(): Promise {
  console.log('vue app bootstraped');
}

export async function mount(props: Props): Promise {
  render(props);

  // 注册全局主应用路由 mainRouter
  // const { mainRouter } = props;
  // app.config.globalProperties.$mainRouter = mainRouter;
}

export async function unmount(): Promise {
  app.unmount();
  app = null;
}
3、打包配置 vue.config.js 修改
const { name } = require('./package.json');
const port = 8082;
const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  publicPath: dev ? '/' : '/first-micro-app/',
  devServer: {
    port,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
}

三、批量打包

1、在主应用与微应用同级新增 package.json 文件
{
  "name": "qiankun-vue3",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "install:all": "node scripts/index.js --serial install",
    "serve:all": "node scripts/index.js --parallel serve",
    "build:all": "yarn run buildApp && yarn run cpApp",
    "buildApp": "node scripts/index.js --serial build",
    "cpApp": "node scripts/build.js" 
  }
}
2、在主应用与微应用同级新增 scripts 文件夹
2.1 批量 install、serve、build

scripts 文件夹中新增 spawn.js 文件

const crossSpawn = require("child_process").spawn;

function kill() {
  const platform = process.platform;
  if (platform === "win32") {
    crossSpawn("taskkill", ["/F", "/T", "/PID", this.pid]);
  } else {
    crossSpawn.kill(this.pid);
  }
}
 
module.exports = function spawn(command, args, options) {
  const child = crossSpawn(command, args, options);
  child.kill = kill;

  return child;
}

scripts 文件夹中新增 index.js 文件

/**
 * argv[1]为 --parallel / --serial
 * argv[2]为 install / serve / build
 */
const path = require('path');
const fs = require('fs');
const spawn = require('./spawn');

const entry = path.resolve();
const microApps = fs.readdirSync(entry).filter(app => /app$/.test(app));

const remove = (array, x) => {
  const index = array.indexOf(x);
  if (index > -1) {
    array.splice(index, 1);
  }
};

const runApp = (app, task) => {
  let cp = null;
  const promise = new Promise((resolve, reject) => {
    const command = 'npm';
    const args = [];
    if (task === 'install') {
      args.push(task);
    } else {
      args.push('run', task);
    }
    
    const { stdin, stdout, stderr } = process;
    const options = {
      stdio: [stdin, stdout, stderr],
    };
    if (app) {
      options.cwd = path.resolve(app);
    }

    cp = spawn(command, args, options);

    cp.on("error", (err) => {
      cp = null;
      reject(err);
    });

    cp.on("close", (code) => {
      cp = null;
      resolve({ app, code });
    });
  });

  // kill pid
  promise.abort = () => {
    if (cp) {
      cp.kill();
      cp = null;
    }
  };

  return promise;
};

const runApps = (apps) => {
  return new Promise((resolve, reject) => {
    if (apps.length === 0) {
      resolve([]);
      return;
    }

    const results = apps.map(app => ({ name: app, code: undefined }))
    const queue = apps.map((app, index) => ({ name: app, index }))
    const promises = [];
    let error = null;
    let aborted = false;

    const done = () => {
      if (error) {
        reject(error);
      }
      resolve(results);
    };

    const abort = () => {
      if (aborted) {
        return;
      }
      aborted = true;
      if (promises.length) {
        for (const p of promises) {
          p.abort();
        }
        Promise.all(promises).then(done, reject);
      } else {
        done();
      }
    };
    
    const next = (task) => {
      if (aborted) {
        return;
      }
      if (!queue.length) {
        if (!promises.length) {
          done();
        }
        return
      }

      const app = queue.shift();
      const promise = runApp(app.name, task);
      promises.push(promise);
      promise.then(
        result => {
          remove(promises, promise);
          
          if (aborted) {
            return;
          }

          results[app.index].code = result.code
          
          if (result.code) {
            error = {
              name: result.app,
              code: result.code,
              results: results,
            };
            abort();
            return;
          }

          next(task);
        },
        err => {
          remove(promises, promise);
          error = err;
          abort();
          return;
        },
      );
    };

    const [mode, task = ''] = process.argv.slice(2);
    
    if (!['--parallel', '--serial'].includes(mode)) {
      const error = 'process.argv第三个参数只能为--parallel、--serial其中之一';
      return reject(error);
    }
    if (!['install', 'serve', 'build'].includes(task)) {
      const error = 'process.argv第四个参数只能为install、serve、build其中之一';
      return reject(error); 
    }
    const len = mode === '--parallel' ? apps.length : 1;
    for (let i = 0; i < len; i++) {
      next(task);
    }
  });
};

// console.log('microApps', microApps);
runApps(microApps)
  .then(result => {
    // console.log('ok');
  })
  .catch(error => {
    console.error('error: ', error);
  });
2.2 批量拷贝 build 后的 dist 文件夹到 外层 dist 目录
/**
 * build
 * 先build到各自文件夹下的dist目录,再拷贝到最外层dist目录
 * 1、先删除上次的dist目录 rm -rf dist
 * 2、拷贝主应用dist到最外层dist cp -rf 20f6e232--cloud--FirstMainMicroApp/dist dist
 * 3、拷贝微应用dist到最外层dist中,且改名为对应微应用名称 cp -rf 20f6e232--cloud--FirstSubMicroApp/dist dist/FirstSubMicroApp
 */
// const { resolve, resolve: pathResolve } = require('path');
const { resolve: pathResolve } = require('path');
const { access, constants, readdirSync } = require('fs');
const { spawn } = require('child_process');

const entry = pathResolve();
// const microApps = readdirSync(entry).filter(app => /app$/.test(app));
const mainAppFolderName = 'main-app';
const microAppFolderName = 'micro-app';
const appNameRegex = new RegExp(`(${mainAppFolderName}|${microAppFolderName})$`)
console.log('appNameRegex', appNameRegex);
const microApps = readdirSync(entry).filter(app => appNameRegex.test(app));
console.log('microApps', appNameRegex, microApps);

// 文件夹是否存在
const isExist = (dir) => {
  return new Promise((resolve, reject) => {
    access(dir, constants.F_OK, err => {
      // err为null时表示存在文件夹dir
      console.log('isExist', err);
      resolve(err);
    })
  })
};

// 删除最外层目录下文件夹
const removeDir = (dir) => {
  return new Promise((resolve, reject) => {
    console.log('removedir', dir);
    const cp = spawn('rm', ['-rf', dir], { cwd: entry });

    cp.on('error', err => {
      console.error(`removeDir err: ${err}`);
      reject(err);
    });

    cp.on('close', code => {
      // 此时code为0
      console.log(`removeDir exited with code ${code}`);
      resolve({ code });
    });
  });
};

// 拷贝文件夹
// cp -rf 20f6e232--cloud--FirstMainMicroApp/dist dist
// cp -rf 20f6e232--cloud--FirstSubMicroApp/dist dist/FirstSubMicroApp
const copyDir = (src, dst) => {
  return new Promise((resolve, reject) => {
    const cp = spawn('cp', ['-rf', src, dst], { cwd: entry });

    cp.on('error', err => {
      reject(err);
    });

    cp.on('close', code => {
      // 此时code为0
      resolve({ code });
    });
  })
};

// 先拷贝主应用dist到最外层目录
const copyMainDir = (dist) => {
  return new Promise((resolve, reject) => {
    const mainApp = microApps.find(app => app.includes(mainAppFolderName));
    const src = pathResolve(mainApp, dist);
    copyDir(src, dist)
      .then(res => resolve(res))
      .catch(err => reject(err));
  });
};

// 再拷贝微应用dist到主应用dist中, 且给微应用dist重命名
const copySubDir = (dist) => {
  const promises = [];
  const subApps = microApps.filter(app => app.includes(microAppFolderName));
  console.log('subApps: ', subApps);
  subApps.forEach(app => {
    let rename = app;
    if (app.includes('--')) {
      const appNames = app.split('--');
      const len = appNames.length;
      rename = appNames[len - 1];
    }
    const src = pathResolve(app, dist);
    const dst = pathResolve(dist, rename);
    const promise = copyDir(src, dst);
    promises.push(promise);
  });

  return promises;
};

// 拷贝主应用与微应用所有dist目录
const copyDirs = (dir) => {
  copyMainDir(dir)
    .then(res => {
      Promise.all(copySubDir(dir))
        .then(res => {
          console.log('复制dist目录成功');
        })
        .catch(err => { 
          console.log('复制微应用dist目录失败', err);
        });
    })
    .catch(err => {
      console.log('复制主应用dist目录失败', err);
    });
};

const buildMicroApps = async (dir) => {
  try {
    const isNull = await isExist(pathResolve(dir));
    if (!isNull) {
      const removeRes = await removeDir(dir);
      console.log('removeRes', removeRes);
      if (!removeRes.code) {
        copyDirs(dir);
      }
    } else {
      copyDirs(dir);
    }
  } catch(err) {
    console.log(err);
  }
};
 
buildMicroApps('dist');
3、参考文件npm-run-all