完美搭配,微前端與 Monorepo 的架構設計

前言

瞭解 Monorepo 與微前端

當想使用一項新的技術時,大量的重構工作會讓你不得不放棄使用新技術的想法時,這時選擇微前端方案,可以大幅降低接入舊項目的成本,使用新技術開發新的功能或重構部分舊的功能模塊。

Monorepo 可以協助微前端項目,因爲它可以提供一個集中式的代碼庫和版本控制系統,使得多個微前端應用可以共享代碼和資源,並且可以更容易地進行協作和集成。通過 Monorepo,可以更容易地管理共享的組件、庫和工具,以及更方便地進行測試、構建和部署。

本文將通過 pnpm monorepo + Micro-App 爲例,爲大家提供一種架構思路。

閱讀本章節可以對 Monorepo 與微前端有一個初步的認識,並通過技術選型對比,選擇更加適合你的組合方案。如果你已經對它們有一定的瞭解,可以跳過本章節。

1. 解決哪些痛點?

2. 實際應用場景

微前端我個人認爲非常適合後臺管理系統開發,尤其是在項目中往往擁有多個業務系統,每個系統都有自己的開發團隊和技術棧,而這些系統之間需要進行數據共享和交互,微前端技術可以讓這些系統更加靈活地集成在一起。

另外,在電商、金融等領域也可以應用微前端技術,例如電商平臺通常會包含多個子系統,如商品管理、訂單管理、支付管理等,這些子系統可以通過微前端技術進行集成,提高系統的整體性能和用戶體驗。

3.Monorepo 技術選型

介紹兩種 monorepo 技術選型方向:

l1p39D

不論是構建型還是輕量型,他們都可以搭配使用,例如 Nx + Lerna + pnpm, 他們可以各司其職。

本文更偏向業務開發方向,推薦使用更輕量得 pnpm 來實現 Monorepo 項目,如果有對版本控制有需求,可以配合 changesets[6] 實現。

4. 微前端技術選型

我最終選擇了 Micro App[12] 微前端框架,它提供了 JS 沙箱、樣式隔離、元素隔離、預加載、資源地址補全、插件系統、數據通信等一系列的完善功能,最主要的是它的接入成本極低,容易上手。

無界沒有深入使用,用法和 Micro App 比較像,都很容易入手,但是 js 沙箱和 css 沙箱實現方式有區別,這點還是根據需求選擇。

技術實戰

技術實戰的方案選擇 pnpm + Micro App 來實現 monorepo 微前端項目,下面我們來一步步實現。

使用 pnpm 實現 monorepo

首先了解一下 monorepo 的組織結構如下:

├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json
├── pnpm-workspace.yaml

在 monorepo 中,所有的包都放在 packages 目錄下,每個包都有自己的 package.json 文件,而根目錄的 package.json 用來管理整個項目的依賴。

安裝 pnpm

pnpm 是一個非常輕量的 monorepo 管理工具,它的安裝非常簡單,只需要執行以下命令即可:

npm install -g pnpm

初始化 monorepo 項目

在項目根目錄下執行以下命令,初始化 monorepo 項目:

pnpm init

創建 pnpm-workspace.yaml 文件,用來配置 monorepo 項目:

packages:
 - 'packages/*'

創建 packages/ 目錄,所有的微前端應用(基座和子應用)和共享模塊都放在這個目錄下。如果你的應用和共享模塊都很多,也可以再加入 apps 目錄區別管理應用和模塊。

安裝 Micro-App

Micro-App 微前端項目中,可以存在多個基座應用,每個基座應用可以包含多個子應用。建議在全局依賴中安裝 Micro-App,這樣所有的基座應用都可以使用它,可以保持基座應用的統一性。子應用無需安裝 Micro-App,後續會介紹如何在子應用中使用 Micro-App。

在項目根目錄下執行以下命令,安裝 Micro-App:

pnpm add micro-app -w

-w 表示安裝到 workspace 根目錄下。

搭建基座應用

這裏我們使用 vite 來搭建基座應用,vite 是一款基於原生 ES Module 的輕量級前端構建工具,它的出現是爲了解決 Webpack 構建速度慢的問題,它的構建速度非常快,可以達到秒級別的構建速度,非常適合用來搭建基座應用。

在項目根目錄下執行以下命令,安裝 vite:

pnpm add vite -D

這裏我們使用 vite 創建一個 vue3 基座應用:

pnpm create vite --template vue packages/base

這裏會提示輸入項目名稱,這裏我們輸入 base,會在 packages 目錄下創建 base 目錄,base 目錄就是基座應用。

搭建子應用

通常來講,微前端對技術不應有要求,但 Micro-App 子應用對 vite 的支持不是很好,所以我們選擇使用 vue-cli 來搭建子應用。

Micro-App 可以使用 vite 開發子應用,但需要一些配置,可以等待 1.0 版本 [13] 的更新。瞭解更多 [14]

這裏我們使用 @vue/cli 初始化一個 vue2 子應用,詳細的 vue-cli 使用可以參考 Vue CLI[15]。

當然你可以使用其他技術棧來搭建子應用,比如 React、Angular、Svelte 等,這裏不做詳細說明。

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,
});

在 src 目錄下創建 public-path.js, 並在 main.js 中引入:

if (window.__MICRO_APP_ENVIRONMENT__) {
 __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}

子應用跨域

如果是開發環境,可以在 webpack-dev-server 中設置 headers 支持跨域。

devServer: {
 headers: {
 'Access-Control-Allow-Origin''*',
 },
},

如果是線上環境,可以通過配置 nginx 支持跨域,後文會有專門對 nginx 配置做詳細講解。

統一的應用配置文件

在 monorepo 中,我們需要統一的應用配置文件,這樣可以方便我們管理所有的應用。根目錄下創建 config.json 文件,用來配置應用:

這裏我們可以列舉幾個關鍵的配置項:

例如:

"base"{
 "name""基座",
 "packageName""@package/core",
 "port"4000
}

其他屬性可以根據需要自行添加。

路由配置

區分環境構建不同的路由

開發環境路徑爲 localhost,而生產環境一般爲域名地址,這裏我們在基座應用創建 src/config.[ts|js] 文件,用來修改不同環境下的地址:

import packageConfig from '@/../../../config.json';
const config = JSON.parse(JSON.stringify(packageConfig));
interface DevConfig {
 [key: string]: string;
}
const devConfig: DevConfig = {};
// 將 config 中的配置項合併到 devConfig 中
Object.keys(config).forEach((key) ={
 if (key !== 'base') {
 devConfig[key] = `http://localhost:${config[key].port}`;
 }
});
// 線上環境地址
if (process.env.NODE_ENV === 'production') {
 // 基座應用和子應用部署在同一個域名下,這裏使用location.origin進行補全
 Object.keys(devConfig).forEach((key) ={
 devConfig[key] = window.location.origin;
 });
}
export default devConfig;

創建路由

在基座應用中創建 src/router/index.ts 文件,用來配置路由:

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
const routes: Array<RouteRecordRaw> = [];
const router = createRouter({
 history: createWebHistory(import.meta.env.BASE_URL),
 routes,
});
export default router;

基座根據配置文件自動生成子應用路由與頁面

編寫方法 tsx 組件,生成子應用頁面組件:

import { defineComponent } from 'vue';
import packageConfig from '@/../../../config.json';
import devConfig from '@/config';
const config = JSON.parse(JSON.stringify(packageConfig));
export default function buildPage(name: string) {
 const url = `${devConfig[name]}/child/${name}`;
 return defineComponent({
 name,
 setup() {
 return () => <div>
 <micro-app
 name={name}
 url={url}
 baseroute={`/base/${name}`}
 disableScopecss={config[name].disableScopecss}
 ></micro-app>
 </div>;
 },
 });
}

編寫 buildMicroRoutes,自動根據配置文件生成子應用路由:

import packageConfig from '@/../../../config.json';
import buildPage from '@/views/buildPage';
const config = JSON.parse(JSON.stringify(packageConfig));
function buildMicroRoutes() {
  const routes: Array<RouteRecordRaw> = [];
  Object.keys(config).forEach((key: string) ={
    if (key !== 'base') {
      routes.push({
        path: `/${key}/:page*`,
        name: key.charAt(0).toUpperCase() + key.slice(1),
        component: buildPage(key),
        meta: { auth: config[key].auth },
      });
    }
  });
  return routes;
}
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'base',
    redirect: '/core',
    children: buildMicroRoutes(),
  },
];

以上代碼僅作參考,可以根據實際情況進行修改。

這麼做的好處是,我們只需要在 config.json 中配置好應用信息,就可以自動生成子應用路由,不需要手動去配置。如果你的應用很少,你可以參考 Micro-App 文檔進行配置。

基座登錄權限管理

通常我們的應用都需要登錄權限管理,這裏我們使用 vue-router[16] 的導航守衛來實現。

首先在根目錄 config.json 中添加 auth 屬性,用來配置是否需要登錄權限,上文中已經在 meta 中增加了 auth 屬性。

在 base/src 目錄下創建 permission.ts 文件,用來配置登錄權限:

import storage from 'store';
import router from './router/index';
import useUserStore from './stores/user';
const loginRoutePath = '/login';
const defaultRoutePath = '/home';
router.beforeEach(async (to, from, next) ={
  const token = storage.get('authKey');
  // 進度條
  NProgress.start();
  // 驗證當前路由所有的匹配中是否需要有登錄驗證的
  if (to.matched.some(r => r.meta.auth)) {
    // 是否存有token作爲驗證是否登錄的條件
    if (token && token !== 'undefined') {
      if (to.path === loginRoutePath) {
        next({ path: defaultRoutePath });
      } else {
        next();
      }
    } else {
      // 沒有登錄的時候跳轉到登錄界面
      // 攜帶上登錄成功之後需要跳轉的頁面完整路徑
      next({
        name: 'Login',
        query: {
          redirect: to.fullPath,
        },
      });
      NProgress.done();
    }
  } else {
    // 不需要身份校驗 直接通過
    next();
  }
});

這裏寫了一個簡單的權限校驗,僅保證用戶是否登錄,複雜情況根據你的實際情況自行處理。

應用之間跳轉

通常我們會在導航欄做頁面的跳轉,這裏分爲兩種情況:

import microApp, { getActiveApps } from '@micro-zoe/micro-app';
if (!getActiveApps().includes(appName)) {
 router.push(`/${appName}${path}`);
} else {
 microApp.setData(appName, { path });
}

getActiveApps 是 Micro-App 提供的方法,獲取正在運行的子應用,不包含已卸載和預加載的應用。

這裏還需要注意的一點是,頁面刷新後,導航欄可能需要重新激活當前的菜單,可以通過子應用向基座發送當前的路由信息,基座在導航組件使用 microApp.addDataListener 方法監聽子應用的信息,從而實現默認激活菜單的功能。

子應用通過導航鉤子去通知基座:

router.afterEach(({ path }) ={
 window.microApp.dispatch({ path });
});

基座通過 microApp.addDataListener 監聽:

microApp.addDataListener(appName, ({ path }) ={
 // ...
});

基座與子應用通訊

Micro-App 提供了幾種通訊方式,這裏列舉幾個常用方法,詳細請參考官方文檔 [17]:

子應用

基座

全局數據

本地存儲

通過本地存儲做全局數據交互,因爲在統一的域名下,本地存儲的數據各個應用都可以獲取到,這裏推薦安裝 store 庫操作本地存儲。

同時,需要注意本地存儲的安全性,避免敏感數據被泄露。可以對存儲的數據進行加密處理,或者只存儲必要的信息,避免存儲過多敏感信息。另外,還需要注意本地存儲的容量限制,避免存儲過多數據導致應用性能下降。

創建 bus 統一管理通訊

推薦在基座應用中創建 bus.js 統一管理與子應用的通訊,通過遍歷所有子應用掛載 addDataListener 方法,對 listener 不同的數據做不同的操作,例如:

具體根據項目需求增加。

由於子應用可以發送任何信息給基座,建議使用 ts 對 listener data 做約束可以更好的管理。

搭建共享通用功能模塊

使用 Monorepo 可以方便地管理多個應用或模塊之間的依賴關係,提高代碼重用性和可維護性。這裏我們可以創建一個 share 模塊,它可以提供給各個應用一些通用的功能。

這裏以封裝 axios 爲例,講解如何搭建通用模塊。

封裝 request

每個應用都會與後端發生請求,那麼一個通用的 request 方法就尤爲重要,否則隨着項目的逐漸壯大,後端在發生變化時,我們前端的維護成本將逐漸變高。

首先整理一下需求:

這裏貼出一些代碼片段,可供參考。

定義後端常用的返回類型,例如列表:

export interface Response<T> {
 errors: Errors;
 response: ResponseResult<T>;
 response_code: number;
 success: boolean;
}
export interface ListResult<T> {
 countId?: string;
 current: number;
 hitCount: boolean;
 maxLimit?: string;
 optimizeCountSql: boolean;
 orders: [];
 pages: number;
 records: T[];
 searchCount: boolean;
 size: number;
 total: number;
}

這樣我們可以在定義接口時直接引用這些類型:

import { request, Response, ListResult } from '@package/share';
interface Result {
 // ...
}
export function getChargingAnalysis() {
 return request<Response<ListResult<Result>>>({
 url: '...',
 method: 'post',
 });
}

應用將 share 作爲依賴

可以在應用的 package.json 中增加依賴:

"dependencies"{
 "@package/share""workspace:*"
},

使用 rollup 打包

使用什麼工具打包都可以,根據喜好自行挑選工具。這裏貼出 rollup 的簡單配置:

import typescript from 'rollup-plugin-typescript2';
import autoprefixer from 'autoprefixer';
import pkg from './package.json';
export default {
  input: 'src/index.ts', // 入口文件
  output: [
    {
      file: pkg.main,
      format: 'esm',
      sourcemap: false,
    },
  ],
  plugins: [
    typescript({
      tsconfigOverride: {
        compilerOptions: { declaration: true, module: 'ESNext' },
      },
      check: false,
    }),
  ],
};

如果要開發支持 vue2/vue3 的組件,推薦使用 vue-demi 開發。

原子化 CSS

原子化 CSS 提供了一系列的樣式類,可以直接應用到 HTML 元素上,減少了手寫 CSS 的時間和工作量。它具有很強的可定製性,可以根據自己的需求自定義樣式,而不必擔心樣式衝突或需要重複編寫 CSS 代碼。

常用的原子化 CSS 框架有:

樣式隔離

微前端中的樣式隔離是爲了防止不同的子應用之間的樣式衝突,保證各個子應用之間的樣式獨立性和隔離性。如果不做樣式隔離,不同子應用使用的樣式可能會相互影響,導致樣式錯亂或者不一致的問題。此外,樣式隔離也可以提高項目的可維護性和可擴展性,方便不同團隊或者開發者獨立開發和維護各自的子應用。因此,微前端中的樣式隔離是非常必要的。

雖然樣式隔離起到了很好的作用,但基座應用的樣式依然會影響到子應用,產生某些不穩定的因素,還是建議關閉樣式隔離,使用原子化 CSS 框架可以極大程度的上解決這種問題。

全局環境變量配置

環境變量是開發時常用的功能,但是在 Monorepo 項目中,每個 package 都存在環境變量,這使得在維護環境變量時變得耗時且不好維護。

接下來我們要做到在根目錄維護環境變量。

不同的構建工具配置方式不同,這裏說一下常見的 vite 和 webpack(@vue/cli) 項目:

Vite

Vite 配置環境變量文件非常方便,因爲它提供了 envDir 屬性:

export default defineConfig({
 envDir: './../../',
 envPrefix: 'VUE_APP',
)};

Webpack

Webpack 沒有提供選擇加載 env 路徑的功能,默認只能加載同級目錄下的文件,但我們使用 dotenv 解決這個問題:

首先安裝 dotenv[22]。

然後編寫一個 setGlobalEnv 方法:

const dotenv = require('dotenv');
const path = require('path');
module.exports = function setGlobalEnv() {
 let envfile = '.env';
 if (process.env.NODE_ENV) {
 envfile += `.${process.env.NODE_ENV}`;
 }
 dotenv.config({
 path: path.resolve('../../', envfile),
 });
};

在 webpack 配置文件或 vue.config.js 中直接調用即可。

這裏存在一個問題還沒有解決,那就是 env.local 暫時無法使用,後續有方法解決會補充。

統一代碼規範

在使用 Monorepo 進行開發時,爲了方便團隊協作和代碼共享,需要制定統一的代碼規範。使用工具如 ESLint、Prettier 等來自動化代碼風格的檢查和格式化。

ESLint

在根目錄下創建 eslint 配置文件,建議使用 .eslintrc.js,可以更方便的匹配不同項目。這裏需要注意的是,在不同的項目中,ESLint 所使用的拓展插件也不同,這時可以通過 overrides 來針對不同路徑的下的項目進行覆蓋:

const path = require('path');
module.exports = {
  overrides: [
    {
      files: ['./packages/vue2/**/*.{js,vue}'],
      extends: ['plugin:vue/essential'],
    },
    {
      files: ['./packages/vue3_ts/src/**/*.{ts,tsx,vue}'],
      extends: ['plugin:vue/vue3-essential''@vue/typescript/recommended'],
    },
  ],
};

上面的例子展示了對應 vue2 和 vue3 + ts 的不同配置。

.eslintignore

由於 node_modules 存在於根目錄和 packages 中,所以應創建 .eslintignore 忽略校驗:

node_modules/*
./node_modules/**
**/node_modules/**

Mock

前端 mock 的作用是在開發過程中模擬數據和接口,使得前端開發人員可以在沒有後端支持的情況下進行開發和調試。通過 mock 數據,前端開發人員可以快速構建出頁面和功能,並且可以在沒有後端接口的情況下進行聯調和測試。同時,mock 數據也可以用於模擬異常情況,以便在開發過程中更好地處理錯誤和異常情況。最終,mock 數據可以提高開發效率,減少開發成本,並且可以更好地保證產品質量。

這裏提供一種基於 msw[23] + faker[24] 實現 mock 接口的方式,這種方式可以絲滑的從 mock 和真實接口進行切換,faker 的輔助提供了更加符合實際的模擬數據。

創建 mock 環境

首先,我們需要安裝 msw 和 faker

pnpm add -w msw faker -D

接下來,我們可以在根目錄創建一個 mocks 目錄,用於存放我們的 mock 接口代碼。

在 mocks 目錄下,創建不同應用的文件夾去創建 mock 請求,這樣更加方便管理,這裏舉一個例子:

// /mocks/task/history.js
import { rest } from 'msw';
import { faker } from '@faker-js/faker/locale/zh_CN';
const baseUrl = process.env.VUE_APP_API_BASE_URL;
export default [
  rest.post(`${baseUrl}/business/history/page`(req, res, ctx) =>
    res(
      ctx.delay(),
      ctx.status(200),
      ctx.json({
        errors: null,
        response: {
          code: 200,
          message: '檢索成功。',
          result: {
            current: req.body.pageNo,
            size: req.body.pageSize,
            total: faker.datatype.number({ min: 100, max: 500 }),
            records: new Array(req.body.pageSize).fill(1).map(() =({
              agencyCode: faker.random.word(),
              createTime: faker.date.past(),
              createUser: faker.datatype.number(),
              dataHash: faker.random.word(),
              id: faker.datatype.uuid(),
              updateTime: faker.date.past(),
              updateUser: faker.datatype.number(),
              vin: faker.vehicle.vin(),
            })),
          },
        },
        response_code: 2000,
        success: true,
      }),
    ),
  ),
];

在 mocks 目錄下,我們可以創建一個 handlers.js 文件,用於整理 handlers:

import taskHistory from './task/history';
export const handlers = [...taskHistory];

在 mocks 目錄下,我們可以創建一個 browser.js 文件,用於啓動瀏覽器環境 mock:

import { setupWorker } from 'msw';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);

隨後需要執行命令:

npx msw init ./packages/base/public --save

這樣會在基座的 public 中創建 mockServiceWorker.js 文件,npx msw init 的作用是初始化 Mock Service Worker 庫,它會在項目根目錄下創建一個 msw 目錄,並在該目錄下創建一個 serviceWorker.js 文件,用於配置和啓動 Mock Service Worker。我們把它放在基座應用下,是因爲子應用也可以直接訪問到 mock 接口。

在應用中使用

建議在 config.json 中增加 mock 開關字段,這樣可以更方便的開啓或關閉 mock。

在應用的 main.js 文件中加入下面的代碼,即可使用:

import config from '../../../config.json';
const microAppName = packageJson.name.split('/')[1];
const isMock = config[microAppName].mock;
if (process.env.NODE_ENV === 'development' && isMock) {
 const { worker } = require('../../../mocks/browser');
 worker.start();
}

搭建技術文檔

項目技術文檔可以幫助項目團隊成員之間更好地協作,減少溝通成本。新成員可以通過文檔瞭解項目的背景和目的,以及項目的技術細節和工作流程,從而更快地融入團隊並開始工作。隨着項目越來越龐大,一個完善的技術文檔,可以爲後續的開發工作提供極大的便利。

這裏推薦使用 vuepress[25] 搭建技術文檔,你只要會 markdown 語法和簡單的配置即可快速搭建。

你也可以閱讀我以前寫過的文章輔助你搭建《VuePress + Travis CI + Github Pages 全自動上線文檔》[26]。

接入舊項目

將舊項目嵌入到微前端應用的過程可能會因項目不同而有所不同,但以下是一些一般步驟:

開發與構建

這一章節我們詳細瞭解一下如何更方便的調試和構建微前端項目。

優化 npm script

當已經有多個應用時,我們通過 npm script 去運行或打包時,需要編輯多條 npm script 命令,這可能會變得很麻煩和複雜。爲了解決這個問題,這裏推薦兩種做法:

npm-run-all

安裝 npm-run-all[27], 它可以讓你同時運行多個 npm script。例如,如果你需要同時運行 "build:base" 和 "build:core",你可以使用以下命令:

"build""npm-run-all build:*",
"build:base""cd packages/base && npm run build",
"build:core""cd packages/core && npm run build"

編寫任務腳本(推薦)

使用 npm-run-all 的方式比較簡單,但也存在一個問題,當我們新增應用時,還需要配置 npm script,並且應用越來越的情況下,package.json 中 npm script 變得巨大且難以維護。

前文提到過,我們所有的應用都在 config.json 中維護,那麼我們創建 node 腳本來啓動或構建應用會更加方便:

我們在根目錄創建 scripts 文件夾,創建 dev.js 和 build.js。

dev.js

首先會判斷基座是否啓動,如果沒啓動,運行會直接先啓動基座,因爲它是必須啓動的。然後出現選擇列表,選擇要啓動的子應用即可,這裏還做了對已啓動的應用做了標記,不可選擇。

JS

// scripts/dev.js
const inquirer = require('inquirer');
const execa = require('execa');
const detect = require('detect-port');
const config = require('../config.json');
// 已佔用端口列表
const occupiedList = [];
const checkPorts = Object.keys(config).map(
  key =>
    new Promise(resolve ={
      detect(config[key].port).then(port ={
        resolve({
          package: key,
          isOccupied: port !== config[key].port,
        });
      });
    }),
);
// 運行選擇命令
function runInquirerCommand() {
  inquirer
    .prompt([
      {
        name: 'devPackage',
        type: 'list',
        message: '請選擇要啓動的子應用',
        choices: Object.keys(config)
          .filter(item => item !== 'base')
          .map(key ={
            const { name, packageName, port } = config[key];
            return {
              name: `${name}(${packageName}:${port})`,
              value: key,
              disabled: occupiedList.find(item => item.package === key).isOccupied
                ? '已啓動'
                : false,
            };
          }),
      },
    ])
    .then(async answers ={
      execa('npm'['run''dev']{
        cwd: `./packages/${answers.devPackage}`,
        stdio: 'inherit',
      });
    });
}
Promise.all(checkPorts).then(ports ={
  occupiedList.push(...ports);
  if (ports[0].isOccupied) {
    runInquirerCommand();
  } else {
    execa('npm'['run''dev']{
      cwd: './packages/base',
      stdio: 'inherit',
    });
  }
});

build.js

微前端具備獨立部署的能力,運行指令後,可以多選要打包的應用,隨後會按順序逐個打包。

// scripts/build.js
const inquirer = require('inquirer');
const execa = require('execa');
const config = require('../config.json');
inquirer
  .prompt([
    {
      name: 'buildPackage',
      type: 'checkbox',
      message: '請選擇要打包的應用',
      choices: Object.keys(config).map(key ={
        const { name, packageName } = config[key];
        return {
          name: `${name}(${packageName})`,
          value: key,
        };
      }),
    },
  ])
  .then(async answers ={
    console.time('打包完成,耗時');
    Promise.all(
      answers.buildPackage.map(
        item =>
          new Promise(resolve ={
            execa('npm'['run''build']{
              cwd: `./packages/${item}`,
              stdio: 'inherit',
            }).then(() ={
              resolve();
            });
          }),
      ),
    ).then(() ={
      console.timeEnd('打包完成,耗時');
    });
  });

最後在 package.json 中配置 npm script:

"dev""node scripts/dev.js",
"build""node scripts/build.js",

構建目錄

爲了方便部署,我們將打包後的路徑改爲根目錄的 dist 目錄下,然後根據基座或子應用放置到不同的文件夾下,最後的結構應該爲:

erlang

複製代碼

├── dist/
|   ├── base/
|   |   ├── ...
|   ├── child/
|   |   ├── child-app1/
|   |   ├── child-app2/
|   |   ├── ...

webpack 可以修改 outputDir 參數,vite 可以修改 build.outDir 即可。

Nginx 配置

微前端在部署時會產生多個地址,可以通過 nginx 的反向代理,把子項目的地址代理到主項目地址下。

這裏貼一下 nginx 配置:

server {
 listen       80;
 server_name  localhost;
 gzip  on; 
 gzip_min_length 1k;
 gzip_comp_level 5; 
 gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
 gzip_disable "MSIE [1-6]\.";
 gzip_vary on;
 location / {
   root /usr/share/nginx/html/base;
   index index.php index.html index.htm;
   # add_header Cache-Control;
   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 *;
   }
 }
 location /base {
   root /usr/share/nginx/html;
   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/ /base/index.html;
 }
 location ^~ /child {
   root /usr/share/nginx/html;
   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/ index.html;
 }
}

需要注意以下幾點:

本機 Docker + Nginx 測試環境搭建

在本地測試環境中,我們可以使用 Docker 和 Nginx 來模擬微前端的生產環境。Docker 可以幫助我們輕鬆地創建和管理容器,而 Nginx 則可以幫助我們將多個應用組合在一起,使它們可以相互通信。

這一步可以大大的減少預生產或生成環境存在未知的 bug(減少扯皮)。

Docker

首先需要安裝 Docker[31],然後在根目錄創建 Dockerfile[32] 文件:

FROM nginx:latest
COPY dist /usr/share/nginx/html
COPY nginx.conf /usr/share/nginx/conf/nginx.conf
EXPOSE 80
CMD ["nginx""-c""/usr/share/nginx/conf/nginx.conf""-g""daemon off;"]

這個 Dockerfile 使用最新版本的 nginx 作爲基礎鏡像,然後將本地的 dist 目錄複製到 nginx 的默認靜態文件目錄 /usr/share/nginx/html 中,再將本地的 nginx.conf 文件複製到 nginx 的默認配置文件目錄 /usr/share/nginx/conf/ 中。最後暴露 80 端口並啓動 nginx 服務。

daemon off 是指關閉 nginx 的守護進程模式,即讓 nginx 在前臺運行而不是在後臺作爲守護進程運行。這通常用於測試或調試時,以便更容易地查看 nginx 的日誌輸出和調試信息。但在實際生產環境中,建議將 nginx 作爲守護進程運行,以確保系統的穩定性和安全性。

Docker Compose

爲了方便管理容器,可以使用 Docker Compose[33],在根目錄下創建 docker-compose.yaml:

services:
 web:
 build:
 context: .
 dockerfile: Dockerfile
 image: web
 container_name: web
 ports:
 - 80:80

這個 docker-compose.yaml 文件定義了一個服務 web,它使用當前目錄下的 Dockerfile 構建鏡像,並將其命名爲 web。容器的名稱也是 web,將本地的 80 端口映射到容器的 80 端口。

然後可以使用以下命令啓動服務:

docker-compose up -d

這會在後臺啓動並運行服務。如果需要停止服務,可以使用以下命令:

docker-compose down

這會停止並刪除服務的容器。如果需要重新構建鏡像並啓動服務,可以使用以下命令:

docker-compose up -d --build

這會重新構建鏡像並啓動服務。

插件

如果你使用 vscode 推薦安裝 Docker[34] 插件,該插件可以方便地管理 Docker 容器、鏡像和服務。它提供了一個 Docker 欄,可以查看容器和鏡像的狀態,並提供了一組命令,可以方便地啓動、停止、刪除和重啓容器。此外,該插件還提供了一個 Docker Compose 欄,可以管理 Docker Compose 項目和服務。安裝 Docker 插件後,可以更輕鬆地進行 Docker 開發和測試。

生產環境部署

如果你已經在本機測試環境下正常啓動,那麼生產環境的配置幾乎一致。

總結

本文主要介紹了微前端和 Monorepo 的架構設計,探討了它們之間如何搭配,並介紹了 Micro App + pnpm 的使用案例,最後,講解了如何使用 Docker + nginx 進行本機測試環境,完整的走完了一個微前端項目的全部流程。

總的來說,微前端 和 Monorepo 的搭配是一種值得嘗試的架構設計,可以幫助團隊更好地管理和開發前端應用。

參考資料

參考資料

[1] https://turbo.build/

[2] https://rushstack.io/

[3] https://nx.dev/

[4] https://lerna.js.org/

[5] https://pnpm.io/zh/workspaces

[6] https://github.com/changesets/changesets

[7] https://github.com/single-spa/single-spa

[8] https://github.com/umijs/qiankun

[9] https://micro-zoe.github.io/Micro-App/

[10] https://wujie-micro.github.io/doc/

[11] https://emp2.netlify.app/

[12] https://micro-zoe.github.io/Micro-App/

[13] https://micro-zoe.com/docs/1.x/#/zh-cn/start

[14] https://micro-zoe.github.io/micro-app/docs.html#/zh-cn/framework/vite

[15] https://cli.vuejs.org/zh/

[16] https://next.router.vuejs.org/zh/

[17] https://micro-zoe.github.io/micro-app/docs.html#/zh-cn/data

[18] https://tailwindcss.com/

[19] https://unocss.dev/

[20] https://cn.vitejs.dev/config/shared-options.html#envdir

[21] https://cn.vitejs.dev/config/shared-options.html#envprefix

[22] https://www.npmjs.com/package/dotenv

[23] https://mswjs.io/

[24] https://fakerjs.dev/

[25] https://v2.vuepress.vuejs.org/

[26] https://juejin.cn/post/6844903869558816781

[27] https://github.com/mysticatea/npm-run-all

[28] https://www.npmjs.com/package/inquirer

[29] https://www.npmjs.com/package/detect-port

[30] https://www.npmjs.com/package/execa

[31] https://www.docker.com/

[32] https://docs.docker.com/engine/reference/builder/

[33] https://docs.docker.com/compose/

[34] https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker

[35] https://micro-zoe.github.io/micro-app/docs.html#/

[36] https://pnpm.io/zh/installation

[37] https://blog.nrwl.io/monorepos-and-react-microfrontends-a-perfect-match-d49dca64489a

關於本文

來源:codexu

https://juejin.cn/post/7225800207329230905

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Ovrp7M-ZvK0V7xO-F3SNiw