微前端

微前端是一种架构模式,它将一个 Web 应用程序分解成多个独立开发和部署的小型应用程序,然后将它们协同工作。

Turborepo 通过集成的代理服务器,为在开发过程中本地运行垂直微前端(有时称为“区域”)提供了内置支持。该代理协调多个应用程序并在它们之间路由流量。

让我们想象一下,你有一个包含多个前端应用程序的 monorepo

package.json
turbo.json

在生产环境中,这些应用程序可能会被单独部署,并使用反向代理或边缘路由进行组合。但在开发过程中,你想要

  • 使用单个命令同时运行所有应用程序
  • 通过统一的 URL(例如 https://:3024)访问它们
  • 根据路径模式将请求路由到正确的应用程序
  • 支持热模块重载和 WebSocket 连接
  • 避免端口冲突和手动协调

入门

Turborepo 提供了一个内置的代理服务器,可在开发过程中自动路由流量到你的微前端应用程序。该代理会读取 microfrontends.json 配置文件,并在你运行 turbo dev 时启动。

你可以使用 npx create-turbo@latest -e with-microfrontends 创建的 monorepo 来尝试以下说明。

创建 microfrontends.json

在你的父应用程序中创建一个 microfrontends.json 文件。当未被其他应用程序匹配时,所有请求将最终落到此应用程序。

./apps/web/microfrontends.json
{
  "$schema": "https://turbo.net.cn/microfrontends/schema.json",
  "applications": {
    "web": {
      "development": {
        "local": {
          "port": 3000
        }
      }
    },
    "docs": {
      "development": {
        "local": {
          "port": 3001
        }
      },
      "routing": [
        {
          "paths": ["/docs", "/docs/:path*"]
        }
      ]
    }
  }
}

设置应用程序端口

接下来,使用 turbo get-mfe-port 命令在应用程序的开发脚本中设置端口。此命令在运行 turbo dev 时注入端口号

./apps/web/package.json
{
  "scripts": {
    "dev": "next dev --port $(turbo get-mfe-port)"
  }
}

处理基础路径

如果你正在使用某个框架,请根据该框架的需求设置基础路径。

./apps/my-app/next.config.ts
import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  basePath: '/docs',
};
 
export default nextConfig;

访问你框架的微前端部分 以获取特定于框架的指导。

运行 turbo dev

当你运行 turbo dev 时,Turborepo 将会

  1. **在配置的端口(默认:3024)上启动代理服务器**
  2. **在任务中注入 TURBO_MFE_PORT 环境变量** 以设置应用程序的端口
  3. **运行所有已配置应用程序的开发任务**
  4. **根据路径模式将传入的请求路由到相应的应用程序**

现在你可以通过 https://:3024 访问你所有的微前端应用程序了。

配置

applications 对象中的每个应用程序都可以具有以下属性

如果未提供,则应用程序的 key 将用于匹配 `package.json` 中的名称。

development.local

应用程序本地运行的端口。

./apps/web/microfrontends.json
{
  "development": {
    "local": {
      "port": 3000
    }
  }
}

如果省略端口,Turborepo 将根据应用程序名称生成一个确定的端口(介于 3000-8000 之间)。

development.fallback

当应用程序未本地运行时,可以选择提供一个目标来代理。这通常用于路由到生产环境,以便像 turbo dev --filter=web 这样只运行部分应用程序的命令也能获得无缝的体验。

./apps/web/microfrontends.json
{
  "development": {
    "fallback": "example.com"
  }
}

routing

一组应该路由到此应用程序的路径组。如果没有提供路由,则该应用程序成为**默认应用程序**,处理所有未匹配的路由。

只能有一个应用程序没有 routing 配置。这是你的“根”应用程序,负责处理其他应用程序未匹配的所有路由。

./apps/web/microfrontends.json
{
  "routing": [
    {
      "paths": ["/api/:version/users/:id"]
    },
    {
      "paths": ["/docs", "/docs/:path*"]
    }
  ]
}
  • 路径是**区分大小写的**:/Blog/blog 是不同的路由
  • 尾部斜杠已标准化:/home/home/ 匹配同一路由

精确匹配

./apps/web/microfrontends.json
{
  "paths": ["/pricing", "/about", "/contact"]
}

这些路径与书写时完全一致。

参数

./apps/web/microfrontends.json
{
  "paths": ["/blog/:slug", "/users/:id/profile"]
}

: 开头的段匹配任何单个路径段。例如,/blog/:slug 匹配 /blog/hello,但不匹配 /blog/hello/world

通配符

./apps/web/microfrontends.json
{
  "paths": ["/docs/:path*", "/api/:version*"]
}

* 结尾的参数匹配零个或多个路径段。例如,/docs/:path* 匹配 /docs/docs/intro/docs/api/reference

你还可以使用 + 修饰符来匹配一个或多个段(不包括空路径)

./apps/web/microfrontends.json
{
  "paths": ["/api/:path+"]
}

这会匹配 /api/users/api/users/123,但不匹配 /api/api/

复杂的路由模式

你可以使用嵌套参数定义复杂的路由

./apps/web/microfrontends.json
{
  "routing": [
    {
      "paths": [
        "/api/:version/users",
        "/api/:version/users/:id",
        "/api/:version/posts/:postId/comments/:commentId"
      ]
    }
  ]
}

分组标签

将路由组织到逻辑组中以便于维护

./apps/web/microfrontends.json
{
  "$schema": "https://turbo.net.cn/microfrontends/schema.json",
  "applications": {
    "marketing": {
      "development": {
        "local": 3002
      },
      "routing": [
        {
          "group": "blog",
          "paths": ["/blog", "/blog/:slug*"]
        },
        {
          "group": "sales",
          "paths": ["/pricing", "/contact", "/demo"]
        }
      ]
    }
  }
}

group 字段仅用于组织目的,不影响路由行为。

packageName

可以选择使用工作区中 package.json 的包名称。

./apps/main/microfrontends.json
{
  "applications": {
    "main-site": {
      "packageName": "web"
    }
  }
}

options.localProxyPort

可以通过 localProxyPort 选项更改代理端口

默认为 3024

./apps/web/microfrontends.json
{
  "options": {
    "localProxyPort": 8080
  }
}

与生产环境集成

Turborepo 的微前端代理仅用于本地使用。你如何实现和集成你的生产微前端取决于你的生产基础设施。但是,我们可以集成你的本地和生产环境,以创建跨环境的无缝体验。

首先,我们构建了 Turborepo 的本地代理以与 Vercel 的微前端集成。我们期待与任何希望集成的基础设施提供商合作。

Vercel 上的微前端

Vercel 对微前端提供原生支持,可提供

  • 具有增强性能的生产代理实现
  • 跨环境的备用 URL 支持
  • Vercel 工具栏集成
  • 功能标志支持
  • 资产前缀处理

在 Vercel 的文档中了解更多信息.

迁移到 @vercel/microfrontends

如果你在任何包中安装了 @vercel/microfrontends 或将其添加到你的工作区,Turborepo 将自动使用它而不是内置代理。这允许渐进式迁移。

你可以使用与 Turborepo 相同的 microfrontends.json 配置。Turborepo 的 microfrontends.json schema 是 Vercel schema 的一个子集,因此它与 @vercel/microfrontends 兼容。

要了解更多关于 @vercel/microfrontends 的信息,请访问 npm 上的包

故障排除

端口已被占用

默认情况下,微前端代理将尝试使用端口 3024。如果你已将该端口用于其他目的,可以使用 options.localProxyPort 更改 Turborepo 的端口。

缺少 CSS、图像或其他资产,或路由不匹配

确保微前端在其 routing 配置中匹配的路径包含资产的路由。检查你的网络选项卡以查找预期匹配或未匹配的路径。

如果你使用单页应用程序(SPA)链接(例如 Next.js 的 <Link> 组件)在应用程序之间进行链接,这可能会导致错误。即使端口和域名相同,应用程序也是不同的。这意味着路由,根据定义,不是“单页应用程序”。

访问代理端口未重定向

你仍然可以直接访问代理端口。例如,如果 localhost:3000 被代理到 localhost:3042,你仍然可以在浏览器中访问 localhost:3000

如果你希望 localhost:3000 重定向到 localhost:3024,你必须在应用程序中手动设置。

应用程序未启动

验证

  1. packageName 与你的实际包名称匹配
  2. 指定的 task 存在于包的 package.json
  3. 每个应用程序的端口可用
  4. 所有依赖项都已安装

turbo get-mfe-port 不工作

如果你在运行 turbo get-mfe-port 时遇到错误,请确保

  1. 你正在从包目录运行命令(而不是从存储库根目录)
  2. 该包在其 package.json 中有一个 name 字段
  3. microfrontends.json 文件存在于某个包中
  4. 当前包列在 microfrontends.jsonapplications 部分
  5. 如果使用 --skip-infer,你还必须指定 --cwd 指向你的存储库根目录
终端
turbo --skip-infer --cwd ../.. get-mfe-port

完整示例

这是一个用于电子商务平台的微前端配置的完整示例

./apps/web/microfrontends.json
{
  "$schema": "https://turbo.net.cn/microfrontends/schema.json",
  "options": {
    "localProxyPort": 3024
  },
  "applications": {
    "web": {
      "packageName": "web",
      "development": {
        "local": {
          "port": 3000
        }
      }
    },
    "docs": {
      "packageName": "documentation",
      "development": {
        "local": {
          "port": 3001
        }
      },
      "routing": [
        {
          "group": "documentation",
          "paths": [
            "/docs",
            "/docs/:path*",
            "/api-reference",
            "/api-reference/:path*"
          ]
        }
      ]
    },
    "blog": {
      "development": {
        "local": {
          "port": 3002
        }
      },
      "routing": [
        {
          "group": "content",
          "paths": [
            "/blog",
            "/blog/:slug",
            "/blog/category/:category",
            "/authors/:author"
          ]
        }
      ]
    },
    "shop": {
      "development": {
        "local": {
          "port": 3003
        }
      },
      "routing": [
        {
          "group": "commerce",
          "paths": [
            "/products",
            "/products/:id",
            "/cart",
            "/checkout",
            "/orders/:orderId"
          ]
        }
      ]
    }
  }
}

有了这个配置

  • web 应用处理主页和任何未匹配的路由
  • docs 应用处理所有文档
  • blog 应用处理博客文章和作者页面
  • shop 应用处理电子商务功能