pnpm + micro-app + Vue3 实现 monorepo微前端项目

9,460 阅读3分钟

背景

公司做了两年多的项目,已经算是个巨石应用了。整个项目中包含了好几个相对独立的功能模块,之前都是独立分布在各个后台管理系统中,为了让用户使用方便,统一账号体系,把它们都揉合到了一个项目里。日积月累,已不堪重负(代码量,打包时间,运行时间,开发体验,维护成本),苦它久矣!现在有机会把项目中的几个功能模块提前出来做商业化,这正是一次重构的机会!

微前端框架选型

我们团队的主要技术栈就是vue,选择micro-app就是看中了它的简洁和侵入性弱,这样接入成本就低了很多,更喜欢它基于WebCompoent的实现原理。

这次vue3没有选择vite作为打包工具,主要还是对vite的微前端方案适配代价巨大,前方不知道还有没有什么坑,所以暂时不去踩坑。 image.png

pnpm 初始化项目

lerna已经停止更新维护了,很多知名的开源项目都选择了pnpm,我们也是紧跟时代潮流。

安装pnpm

npm i pnpm -g

初始化项目:

pnpm init -y

新建一个pnpm-workspace.yaml文件:

packages:
  - 'packages/**'

新建一个packages目录,在里面建立几个模块:

├── packages
|   ├── base  // 基座
|   |   ├── package.json
|   ├── dataCenter  // 数据中心
|   |   ├── package.json
|   ├── share // 共享模块
|   |   ├── package.json
├── package.json

初始化vue项目

packages/base目录下创建一个vue3 + ts的项目。因为我们技术栈都是vue,我们可以将公共使用的依赖拷贝到根目录下的package.json中,进行统一安装。

  "dependencies": {
    ...
    "@element-plus/icons-vue": "^2.0.1",
    "axios": "^0.27.2",
    "element-plus": "^2.2.2",
    "pinia": "^2.0.14",
    "vue": "^3.2.25",
    "vue-router": "^4.0.12"
    ...
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^4.18.0",
    "@typescript-eslint/parser": "^4.18.0",
    ...
    "babel-loader": "^8.2.5",
    ...
  }

在根目录下执行命令:

pnpm i -w // -w, --workspace-root 表示在根目录下安装

初始化公共模块

packages 下新建一个share目录:

├── share
|   ├── components  // 公共组件
|   ├── style       // 公共样式
|   ├── utils       // 公共方法
├── package.json

通过link机制实现package包之间的依赖引用:

pnpm i pnpm i [packageName]@workspace --filter [packageName] // 通过filter 过滤需要关联依赖的package
  "dependencies": {
    ...
    "@ultra/share": "workspace:^0.1.0", // 通过 workspace协议 为本地引用
    ...
  },

我们可以在share/components下新建一个测试用的组件:

<template>
  <div>
    这是公共组件 {{msg}}
  </div>
</template>

<script setup lang="ts">
  import { defineProps } from 'vue';
  const props = defineProps({
    msg: {
      type: String,
      default: 'Hello World!'
    }
  });
</script>
// packages/base/scr/app.vue
<template>
    <TestComponent/>
</template>
<script setup lang="ts">
import TestComponent from '@ultra/share/components/test.vue';
</script>

image.png

其他share目录下的模块,也用类似的方式引用:@ultra/share/style/index.less,@ultra/share/utils/index.ts

micro-app 微前端实现

关于micro-app的安装配置内容,建议前往官方文档,查看对应框架的配置说明,手把手教学,简单易上手,侵入性低。官方文档!

实现基座

  • base项目中安装micro-app
pnpm i @micro-zoe/micro-app --filter @ultra/base
  • 在入口里引入:
// main.ts

import microApp from '@micro-zoe/micro-app';
microApp.start();
createApp(App).use(router).mount('#app');
  • 分配一个子路由:
const routes: Array<RouterRecordRaw> = [
  ...
  {
    // 👇 非严格匹配,/popularize/* 都指向 Popularize 页面
    path: '/popularize/:page*',
    name: 'Popularize',
    component: () => import('../views/popularize.vue')
  }
  ...
]
<template>
  <div>
    <!--
      url: nginx 配置的 子项目路径
      baseroute: nginx 配置的 主项目路径 + 项目中子项目的路由入口
     -->
    <micro-app
      name='popularize'
      :url='url'
      baseroute='/micro/popularize'
      @datachange='handleDataChange'></micro-app>
  </div>
</template>

<script setup lang="ts">
  const url = process.env.NODE_ENV === 'development' ? 'http://localhost:8888/popularize' : 'http://www.finget.xyz/popularize';
</script>

配置子应用

  • 配置跨域支持:
// vue.config.js
devServer: { 
  headers: { 
    'Access-Control-Allow-Origin': '*', 
  } 
}
  • 设置基础路由:如果基座是history路由,子应用是hash路由,这一步可以省略
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import routes from './router'

const router = createRouter({
  // 👇 __MICRO_APP_BASE_ROUTE__ 为micro-app传入的基础路由
  history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL),
  routes,
})
  • 设置 publicPath:
// publicPath.ts
// __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量 
// @ts-ignore
if (window.__MICRO_APP_ENVIRONMENT__) { 
  // @ts-ignore
  __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
// main.ts
import './publicPath.ts';

运行项目

在根目录下的package.json中新增脚本:

"scripts": {
  "serve": "pnpm run --parallel serve",
  "build": "pnpm run --parallel build"
}

通过--parallel会执行所有package中的serve命令。更多pnpm 命令的配置请查看官方文档

部署

官方强烈建议我们保持开发环境和线上环境路径(即webpack的publicPath)的一致性。 image.png

所以先考虑好上线部署的目录结构:

├── html
|   ├── micro      // 基座
|   ├── popularize // 子应用
|   ├── child1     // 子应用

image.pngvue.config.js中需要配置好对应的publicPath:

module.exports = {
  publicPath: '/micro/'
}

主应用的路由中还得配置一下baseRoute:

const router = createRouter({
  history: createWebHistory('/micro'),
  routes
});

nginx配置:

location /popularize {
    alias   html/popularize; 
    add_header Access-Control-Allow-Origin *;
    if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
       add_header Cache-Control max-age=7776000;
       add_header Access-Control-Allow-Origin *;
     }
     try_files $uri $uri/ /popularize/index.html;
     index index.html index.htm;
}
location /micro {
  alias   html/micro; 
  try_files $uri $uri/ /micro/index.html;
  index index.html index.htm;
}

线上体验地址: www.finget.xyz/micro/home (服务器配置低,访问有点慢!)

仓库地址:github.com/FinGet/micr…