微前端项目(qiankun+monorepo)

2,017 阅读5分钟

微前端项目(qiankun+monorepo)

monorepo是什么?

monorepo 是monolithic repository的缩写,意为 "单一仓库"。

它是一个存放多个软件包的单一仓库,每个包有自己的 package.json 文件,通过 package 目录进行组织。

monorepo相比于multirepo的优势

  1. 统一的依赖管理。由于所有包都在同一个仓库,可以在仓库根目录统一安装依赖,然后进行 symlinks 链接,实现依赖复用。这避免了每个仓库都依赖相同模块的不同版本,节省空间与管理成本。
  2. 简单的跨包协作。在同一个仓库下开发,可以很容易的在不同包之间协作与重用代码。
  3. 一致的 Commit 历史。monorepo 下的所有包共享完全相同的提交历史和更改记录。这使得审查更改和依赖关系变得非常简单。
  4. 简化的工作流。由于一个仓库对应所有包,我们可以应用一致的 Git 分支模型、Issue 标签体系、Pull Request 流程与 CI 配置。这大大简化了多个仓库才需要的额外工作。

缺点:

但 monorepo 也有一定弊端,如仓库体积太大不易管理,而且并非所有的构建工具与包管理器都原生支持 monorepo 项目。

搭建monorepo项目

  1. 安装pnpm(monorepo需要特定的构建工具和包管理器)
  2. 初始化项目:新建一个文件夹,初始化一个默认的package.json (子项目共用的package.json)
  3. 根目录创建 pnpm-workspace.yaml 文件。 这个文件可以帮助我们在安装公共依赖的情况下,也将 packages 下的项目所需要的依赖也同时进行安装(让子项目需要的特殊依赖也同时安装)(workspace协议)
  4. 新建packages文件夹,来存放项目,进入package项目,初始化项目:(初始化一个子项目 pnpm create vite vue-demo1 --template vue-ts )
  5. 进入到子项目,将提取到公共依赖项的依赖删除。(修改子项目的package.json)。
  6. 在根目录执行pnpm i。
  7. 通过命令启动子项目。

注:全局安装依赖项: 如果想在根目录添加依赖,比如添加 element-plus需要在命令行后面加-w : (pnpm install element-plus -w)

项目结构目录

image.png

// pnpm-workspace.yaml
packages:
  - 'packages/*'
// package.json
{
  "name": "vue3-pnpm-monorepo",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "prepare": "husky install",
    "dev:vue-demo1": "pnpm -C ./packages/vue-demo1 dev",
    "dev:vue-demo2": "pnpm -C ./packages/vue-demo2 dev",
    "dev:vue-demo3": "pnpm -C ./packages/vue-demo3 dev",
    "lint:create": "eslint --init",
    "lint": "eslint \"packages/**/*.{js,vue,ts}\" ",
    "prettier-format": "prettier --config .prettierrc.js \"packages/**/*.{js,vue,ts}\" --write"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.1.0",
    "element-plus": "^2.3.4",
    "eslint-config-prettier": "^8.8.0",
    "qiankun": "^2.10.8",
    "vue": "^3.2.47"
  },
  "devDependencies": {
    "@commitlint/cli": "^17.6.3",
    "@commitlint/config-conventional": "^17.6.3",
    "@types/node": "^18.11.12",
    "@typescript-eslint/eslint-plugin": "^5.4.0",
    "@typescript-eslint/parser": "^5.4.0",
    "eslint": "^8.40.0",
    "eslint-plugin-vue": "^9.13.0",
    "husky": "^8.0.3",
    "lint-staged": "^13.2.2",
    "npm-run-all": "^4.1.5",
    "prettier": "^2.8.8",
    "shelljs": "^0.8.4",
    "typescript": "^5.0.2",
    "vite-plugin-eslint": "^1.8.1"
  },
  "lint-staged": {
    "*.{js,jsx,vue,ts,tsx}": [
      "pnpm run lint",
      "pnpm run prettier-format"
    ]
  },
  "license": "ISC"
}
// 子项目的package.json
  {
    "name": "vue-demo1",
    "private": true,
    "version": "0.0.0",
    "type": "module",
    "scripts": {
    "dev": "vite",
    "build: pre": "vue-tsc && vite build --mode dev",
    "build: pro": "vue-tsc && vite build --mode pro",
    "preview": "vite preview"
    },
    "dependencies": {
    "echarts": "^5.4.2",
    "vite-plugin-qiankun": "^1.0.15",
    "vue": "^3.2.47"
    },
    "devDependencies": {
    "@vitejs/plugin-vue": "^4.1.0",
    "typescript": "^5.0.2",
    "vite": "^4.3.2",
    "vue-tsc": "^1.4.2"
    }
}

微前端--使用路由控制微应用

主应用: vue3+vite(vue-base)

子应用1: vue3+vite(vue-demo1)

子应用2: vue3+vite(vue-demo2)

子应用3: angular+webpack(angularApp)

在主应用注册子应用

```
1 安装qiankun依赖: pnpm install qiankun

2 注册子应用:在src文件下新建qiankun文件夹,新建index.js文件

3  index.js文件内容
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
  {
    name: 'vue-app', // 必须与微应用注册名字相同
    entry: '//localhost:9999', // 入口路径,开发时为微应用所启本地服务,上线时为微应用线上路径
    container: '#container', // 微应用挂载的节点
    activeRule: '/micro-vue',// 当访问路由为 /micro-vue 时加载微应用
    props:{
      msg: "我是第一个微应用" // 主应用向微应用传递参数
    }
  },
  {
    name: 'angular-app',
    entry: '//localhost:9000',
    container: '#container', // 微应用挂载的节点
    activeRule: '/micro-angular',
  },
]);
​
start({
  prefetch: 'all', // 预加载
  sandbox: {
    experimentalStyleIsolation: true, //   开启沙箱模式,实验性方案
  },
});
​
4 在主应用的main.ts里面引入这个文件: import "./qiankun"
    import 'zone.js'; // zone.js要放在最上面,不然页面卡死
    import { createApp } from 'vue'
    import App from './App.vue'
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    import * as ElementPlusIconsVue from '@element-plus/icons-vue'
    import './assets/main.css'
    import { createPinia } from 'pinia'
​
​
    import router from './router'
    import './qiankun'
​
    const app = createApp(App)
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
      app.component(key, component)
    }
    app.use(ElementPlus , { size: 'small', zIndex: 3000 })
    app.use(createPinia())
    app.use(router)
​
    app.mount('#app')
​

5 在 App.vue里面挂载节点
<script setup></script>
<template>
  <div>
    <div id="containerVue" />   // 我是子应用
  </div>
</template>
<style scoped></style>
​
```

微应用配置

微应用是vue3+vite(vue-demo1)
1、安装vite插件(qiankun 目前是不支持vite的。需要借助插件)
// pnpm install vite-plugin-qiankun2、修改vite.config.js.
// import qiankun from 'vite-plugin-qiankun'
export default defineConfig({
plugins: [
 vue(),
 qiankun('vue-app', { // 微应用名字,与主应用注册的微应用名字保持一致
   useDevMode: true
 })
],
server: {
 origin: 'http://localhost:9999', // 项目baseUrl,解决主应用中出现静态地址404问题
 host: 'localhost',
 port: 9999,
 open: true, 
},
})
​
3、修改main.ts入口文件
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
import { renderWithQiankun, qiankunWindow, QiankunProps } from 'vite-plugin-qiankun/dist/helper'const initQianKun = () => {
renderWithQiankun({
 // 当前应用在主应用中的生命周期
 // 文档 https://qiankun.umijs.org/zh/guide/getting-started#
 mount(props) {
   render(props.container)
   //  可以通过props读取主应用的参数:msg
   // 监听主应用传值
   props.onGlobalStateChange((res: { count: any }) => {
     // console.log(res.count)
   })
 },
 bootstrap() { },
 unmount() { },
 update: function (props: QiankunProps): void | Promise<void> {
   throw new Error('Function not implemented.')
 }
})
}
​
const render = (container?: HTMLElement | undefined) => {
// 如果是在主应用的环境下就挂载主应用的节点,否则挂载到本地
const appDom = container ? container : '#app'
createApp(App).mount(appDom)
}
​
// 判断当前应用是否在主应用中
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render()
​
 微应用是angular
 1 新建 angular子项目 ng new name
 2 新增 public-path.js文件.内容为
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
 3 修改入口文件,src/main.ts 文件。
import './public-path';
import { enableProdMode, NgModuleRef } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
​
let app: void | NgModuleRef<AppModule>;
async function render() {
  app = await platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
}
if (!(window as any).__POWERED_BY_QIANKUN__) {
  render();
}
​
export async function bootstrap(props: Object) {
  console.log(props);
}
​
export async function mount(props: Object) {
  render();
}
​
export async function unmount(props: Object) {
  console.log(props);
  // @ts-ignore
  app.destroy();
}
​
 4 修改webpack打包配置,
// 需要安装@angular-builders/custom-webpack 插件 
// 注意: 插件版本要和angular保持一致
// 在根目录新增custom-webpack.config.js内容为:
const appName = require('./package.json').name;
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  output: {
    library: `${appName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${appName}`,
  },
};
// 修改 angular.json中打包配置,加入我们的打包配置文件
- "builder": "@angular-devkit/build-angular:browser", // 删除
+ "builder": "@angular-builders/custom-webpack:browser",
  "options": {
+    "customWebpackConfig": {
+      "path": "./custom-webpack.config.js"
+    }
  }
      
- "builder": "@angular-devkit/build-angular:dev-server", // 删除
+ "builder": "@angular-builders/custom-webpack:dev-server",
      
 5 解决zone.js 
// 在父应用引入 zone.js,需要在 import qiankun 之前引入。(顶部引用,防止angular项目卡死)
// 将微应用的 src/polyfills.ts 里面的引入 zone.js 代码删掉
      
 6  为了防止主应用或其他微应用也为 angular 时,建议给<app-root> 加上一个唯一的 id,比如说当前应用名称。
​
src/index.html :
+ <app-root id="angular9"></app-root>
      
src/app/app.component.ts :
+ selector: '#angular9 app-root',