For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt, and this page is available as Markdown at /zh/guides/advanced-features/bff/function.md.
  • 简体中文
  • 基础用法

    在 Modern.js 应用中,开发者可以在 api/lambda 目录下定义接口文件,并导出接口函数。在前端代码中,可以用文件引用的方式,直接调用这些接口函数,发起接口请求。

    这种调用方式我们称为一体化调用,开发者无需编写前后端胶水层代码,并天然地保证前后端类型安全。

    启用 BFF

    要在 Modern.js 项目中启用 BFF 功能,请按照以下步骤修改代码:

    1. 安装 BFF 插件依赖

    如果项目尚未安装 BFF 插件,请先安装:

    pnpm add @modern-js/plugin-bff
    版本一致性

    请确保 @modern-js/plugin-bff 的版本与项目中 @modern-js/app-tools 的版本保持一致。Modern.js 的所有官方包使用统一版本号发布,版本不一致可能导致兼容性问题。

    请先查看 @modern-js/app-tools 的版本,然后安装相同版本的 @modern-js/plugin-bff:

    # 查看当前 @modern-js/app-tools 的版本
    pnpm list @modern-js/app-tools
    
    # 安装相同版本的 @modern-js/plugin-bff
    pnpm add @modern-js/plugin-bff@<版本号>
    1. 配置 modern.config.ts

    在 modern.config.ts 文件中导入并添加 BFF 插件:

    modern.config.ts
    import { defineConfig, appTools } from '@modern-js/app-tools';
    import { bffPlugin } from '@modern-js/plugin-bff';
    
    export default defineConfig({
      plugins: [appTools(), bffPlugin()],
    });
    1. 配置 TypeScript 别名

    为了在 TypeScript 中正确识别 @api 别名,建议在 tsconfig.json 中添加路径映射:

    tsconfig.json
    {
      "compilerOptions": {
        "paths": {
          ..., // other paths,
          "@api/*": ["./api/lambda/*"]
        }
      },
      "include": [
        ..., // 其他 include 目录
        "api"  // 添加 api 目录
      ]
    }

    BFF 函数

    允许使用一体化调用的函数,我们称为 BFF 函数。这里写一个最简单的 BFF 函数,首先创建 api/lambda/hello.ts 文件:

    api/lambda/hello.ts
    export const get = async () => 'Hello Modern.js';

    接着在 src/routes/page.tsx 中直接引入函数并调用:

    src/routes/page.tsx
    import { useState, useEffect } from 'react';
    import { get as hello } from '@api/hello';
    
    export default () => {
      const [text, setText] = useState('');
    
      useEffect(() => {
        hello().then(setText);
      }, []);
      return <div>{text}</div>;
    };

    在 src/routes/page.tsx 中引入的函数,会自动转换成接口调用,不需要再通过请求 SDK 或 Web Fetch 调用接口。

    执行 pnpm run dev 后,打开 http://localhost:8080/ 可以看到页面已经展示了 BFF 函数返回的内容,在 Network 中可以看到页面向 http://localhost:8080/api/hello 发送了请求:

    Network

    函数路由

    Modern.js 中,BFF 函数对应的路由系统是基于文件系统实现的,也是一种约定式路由。api/lambda 下的所有文件中的每个 BFF 函数都会映射为一个接口,下面介绍几种路由的约定。

    Info

    所有 BFF 函数生成的路由都带有统一的前缀,默认值为 /api。可以通过 bff.prefix 设置公共路由的前缀。

    默认路由

    以 index.ts 命名的文件会被映射到上一层目录。

    • api/lambda/index.ts -> {prefix}/
    • api/lambda/user/index.ts -> {prefix}/user

    多层路由

    支持解析嵌套的文件,如果创建嵌套文件夹结构,文件仍会以相同方式自动解析路由。

    • api/lambda/hello.ts -> {prefix}/hello
    • api/lambda/user/list.ts -> {prefix}/user/list

    动态路由

    同样的,创建命名带有 [xxx] 的文件夹或者文件,支持动态的命名路由参数。动态路由的函数参数规则可以看 dynamac-path。

    • api/lambda/user/[username]/info.ts -> {prefix}/user/:username/info
    • api/lambda/user/username/[action].ts -> {prefix}/user/username/:action

    白名单

    默认 api/lambda/ 目录下所有文件都会当作 BFF 函数文件去解析,但以下文件不会被解析:

    • 命名以 _ 开头的文件。例如:_utils.ts。
    • 命名以 _ 开头的文件夹下所有文件。例如:_utils/index.ts、_utils/cp.ts。
    • 测试文件。例如:foo.test.ts。
    • TypeScript 类型文件。例如:hello.d.ts。
    • node_module 下的文件。

    RESTful API

    Modern.js 的 BFF 函数需要遵循 RESTful API 标准来定义,开发者需要按照一系列规则来定义 BFF 函数。

    设计原则

    BFF 函数不仅会在项目中被调用,也应该允许其他项目通过请求 SDK 或 Web fetch 调用。因此 Modern.js 没有在一体化调用时定义私有协议,而是通过标准的 HTTP Method,以及 params、query、body 等通用的 HTTP 请求参数来定义函数。

    函数导出规则

    HTTP Method 具名函数

    Modern.js BFF 函数的导出名决定了函数对应接口的 HTTP Method,如 get,post 等。例如导出一个 GET 接口:

    export const get = async () => {
      return {
        name: 'Modern.js',
        desc: '现代 web 工程方案',
      };
    };

    按照以下例子,则可导出一个 POST 接口:

    export const post = async () => {
      return {
        name: 'Modern.js',
        desc: '现代 web 工程方案',
      };
    };
    • 对应 HTTP Method,Modern.js 也支持了 9 种定义,即:GET、POST、PUT、DELETE、CONNECT、TRACE、PATCH、OPTIONS、HEAD,即可以用这些 Method 作为函数导出的名字。

    • 名字是大小不敏感的,如果是 GET,写成 get、Get、GEt、GET,都可以准确识别。而默认导出,即 export default xxx 则会被映射为 Get。

    使用 Async 函数

    Modern.js 推荐将 BFF 函数定义为 Async 异步函数,即使函数中不存在异步流程,例如:

    export const get = async () => {
      return {
        name: 'Modern.js',
        desc: '现代 web 工程方案',
      };
    };

    这是因为在前端调用时,BFF 函数会自动转换成 HTTP 接口调用,而 HTTP 接口调用时异步的,在前端通常会这样使用:

    src/routes/page.tsx
    import { useState, useEffect } from 'react';
    import { get as hello } from '@api/hello';
    
    export default () => {
      const [text, setText] = useState('');
    
      useEffect(() => {
        hello().then(setText);
      }, []);
      return <div>{text}</div>;
    };

    因此,为了保持类型定义与实际调用体验统一,我们推荐在定义 BFF 函数时将它设置为异步函数。

    函数参数规则

    函数参数规则分为两块,分别是请求路径中的动态路由(Dynamic Path)和请求选项(RequestOption)。

    Dynamic Path

    动态路由会作为 BFF 函数第一部分的入参,每个入参对应一段动态路由。例如以下示例,level 和 id 会作为前两个参数传递到函数中:

    api/lambda/[level]/[id].ts
    export default async (level: number, id: number) => {
      const userData = await queryUser(level, uid);
      return userData;
    };

    在调用时直接传入动态参数:

    src/routes/page.tsx
    import { useState, useEffect } from 'react';
    import { get as getUser } from '@api/[level]/[id]';
    
    export default () => {
      const [name, setName] = useState('');
    
      useEffect(() => {
        getUser(6, 001).then(userData => setName(userData.name));
      }, []);
    
      return <div>{name}</div>;
    };

    RequestOption

    Dynamic Path 之后的参数是包含 querystring、request body 的对象 RequestOption,这个字段用来定义 data 和 query 的类型。

    在不存在动态路由的普通函数中,可以从第一个入参中获取传入的 data 和 query,例如:

    api/lambda/hello.ts
    import type { RequestOption } from '@modern-js/plugin-bff/server';
    
    export async function post({
      query,
      data,
    }: RequestOption<Record<string, string>, Record<string, string>>) {
      // do somethings
    }

    这里你也可以使用自定义类型:

    api/lambda/hello.ts
    import type { RequestOption } from '@modern-js/plugin-bff/server';
    
    type IQuery = {
      // some types
    };
    type IData = {
      // some types
    };
    
    export async function post({ query, data }: { query: IQuery; data: IData }) {
      // do somethings
    }

    当函数文件使用动态路由规则时,动态路由会在 RequestOption 对象参数前。

    api/lambda/[sku]/[id]/item.ts
    export async function post(
      sku: string,
      id: string,
      {
        data,
        query,
      }: RequestOption<Record<string, string>, Record<string, string>>,
    ) {
      // do somethings
    }

    调用时也按照函数定义,传入对应的参数即可:

    src/routes/page.tsx
    import { post } from '@api/[sku]/[id]/item';
    
    export default () => {
      const addSku = () => {
        post('0001' /* sku */, '1234' /* id */, {
          query: {
            /* ... */
          },
          data: {
            /* ... */
          },
        });
      };
    
      return <div onClick={addSku}>添加 SKU</div>;
    };

    扩展 BFF 函数

    普通的 BFF 函数写法有时并不能满足需求,例如业务场景下复杂的 TS 类型需要,Modern.js 提供了一种更强大的 BFF 函数写法。

    详细内容可参考 - 创建可扩展的 BFF 函数。

    代码共享

    除 api/ 目录下的 BFF 函数可在 src/ 中通过一体化调用方式引用,项目中 src/ 和 api/ 目录默认不能直接引用对方代码。为实现代码共享,可在项目根目录创建 shared 目录,供 src/ 和 api/ 共同引用。