开发主应用

在上一章 体验微前端 通过一个示例演示了如何创建和配置微前端子应用,通过本章你可以进一步了解如何开发主应用,以及它的常见配置。

在通过 @modern-js/create 命令创建 Modern.js 工程后,可以在项目中执行 pnpm run new(实际执行了 modern new 命令),在选择了「微前端」模式后,会安装微前端依赖依赖,只需手动注册插件即可。

modern.config.ts
import { appTools, defineConfig } from '@modern-js/app-tools';
import { garfishPlugin } from '@modern-js/plugin-garfish';

export default defineConfig({
  runtime: {
    router: true,
    masterApp: {
      apps: [{
        name: 'Table',
        entry: 'http://localhost:8081',
        // activeWhen: '/table'
      }, {
        name: 'Dashboard',
        entry: 'http://localhost:8082'
        // activeWhen: '/dashboard'
      }]
    },
  },
  plugins: [appTools(), garfishPlugin()],
});

注册子应用信息

当在 masterApp 配置上提供了信息后,将会认为该应用为主应用,目前存在两种子应用信息的配置方式,这两种方式分别应用于不同的场景。

直接注册子应用信息

可以直接通过配置注册子应用信息:

提示

可以通过 API defineConfig 在运行时进行配置。 当参数为函数时无法被序列化到产物代码,所以在涉及到函数之类的配置时请通过 defineConfig 来进行配置

src/App.tsx
import { defineConfig } from '@modern-js/runtime';

defineConfig(App, {
  masterApp: {
    apps: [{
      name: 'Table',
      entry: 'http://localhost:8081',
      // activeWhen: '/table'
    }, {
      name: 'Dashboard',
      entry: 'http://localhost:8082'
      // activeWhen: '/dashboard'
    }]
  },
});

自定义远程应用列表

通过该函数可以拉取远程的子应用列表,并将其注册至运行时框架:

App.tsx
defineConfig(App, {
  masterApp: {
    manifest: {
      getAppList: async () => {
        // 可以从其他远程接口获取
        return [
          {
            name: 'Table',
            entry: 'http://localhost:8081',
            // activeWhen: '/table'
          },
          {
            name: 'Dashboard',
            entry: 'http://localhost:8082',
            // activeWhen: '/dashboard'
          },
        ];
      },
    },
  },
});

渲染子应用

在微前端中分为两种加载子应用的方式:

  1. 子应用组件 获取到每个子应用的组件,之后就可以像使用普通的 React 组件一样渲染微前端的子应用。
  2. 集中式路由 通过集中式的路由配置,自动根据当前页面 pathname 激活渲染对应的子应用。

子应用组件

开发者使用 useModuleApps 方法可以获取到各个子应用的组件。

再通过 router 组件的结合运用,开发者可以自控式的根据不同的路由渲染不同的子应用。

假设我们的子应用列表配置如下:

modern.config.ts
import { appTools, defineConfig } from '@modern-js/app-tools';
import { garfishPlugin } from '@modern-js/plugin-garfish';

export default defineConfig({
  runtime: {
    router: true,
    masterApp: {
      apps: [{
        name: 'Table',
        entry: 'http://localhost:8081',
        // activeWhen: '/table'
      }, {
        name: 'Dashboard',
        entry: 'http://localhost:8082'
        // activeWhen: '/dashboard'
      }]
    },
  },
  plugins: [appTools(), garfishPlugin()],
});

编辑主应用 App.tsx 文件如下:

App.tsx
import { useModuleApps } from '@modern-js/plugin-garfish/runtime';

import {
  RouterProvider,
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  BrowserRouter,
  Link,
  Outlet,
} from '@modern-js/runtime/router';

const AppLayout = () => (
  <>
    <div>
      <Link to={'/table'}>加载约定式路由子应用</Link>
    </div>
    <div>
      <Link to={'/dashboard'}>加载自控式路由子应用</Link>
    </div>
    <div>
      <Link to={'/'}>卸载子应用</Link>
    </div>
    <Outlet />
  </>
);

export default () => {
  const { apps, MApp } = useModuleApps();

  // 使用的不是 MApp 组件,需要使用 createBrowserRouter 来创建路由
  const router = createBrowserRouter(
    createRoutesFromElements(
      <Route path="/" element={<AppLayout />}>
        {apps.map(app => {
          const { Component } = app;
          // 模糊匹配,path 需要写成类似 abc/* 的模式
          return (
            <Route
              key={app.name}
              path={`${app.name.toLowerCase()}/*`}
              element={
                <Component
                  loadable={{
                    loading: ({ pastDelay, error }: any) => {
                      if (error) {
                        return <div>error: {error?.message}</div>;
                      } else if (pastDelay) {
                        return <div>loading</div>;
                      } else {
                        return null;
                      }
                    },
                  }}
                />
              }
            />
          );
        })}
      </Route>,
    ),
  );

  return (
    // 方法一:使用 MApp 自动根据配置的 activeWhen 参数加载子应用(本项目配置在 modern.config.ts 中)
    // <BrowserRouter>
    //   <MApp />
    // </BrowserRouter>

    // 方法二:手动写 Route 组件方式加载子应用,方便于需要鉴权等需要前置操作的场景
    <>
      <RouterProvider router={router} />
    </>
  );
};

这里通过 Route 组件自定义了 Table 的激活路由为 /tableDashboard 的激活路由为 /dashboard

集中式路由

集中式路由 是将子应用的激活路由集中配置的方式。我们给子应用列表信息添加 activeWhen 字段来启用 集中式路由

src/App.tsx
import { defineConfig } from '@modern-js/runtime';

defineConfig(App, {
  masterApp: {
    apps: [{
      name: 'Table',
      entry: 'http://localhost:8081',
      // activeWhen: '/table'
    }, {
      name: 'Dashboard',
      entry: 'http://localhost:8082'
      // activeWhen: '/dashboard'
    }]
  },
});

然后在主应用中使用 useModuleApp 方法获取 MApp 组件, 并在主应用渲染 MApp

主应用:App.tsx
import { useModuleApp } from '@modern-js/plugin-garfish/runtime';

function App() {
  const { MApp } = useModuleApps();

  return (
    <div>
      <MApp />
    </div>
  );
}

这样启动应用后,访问 /table 路由,会渲染 Table 子应用,访问 /dashboard 路由,会渲染 Dashboard 子应用。

两种模式混用

当然 子应用组件集中式路由 是可以混合使用的。

  • 一部分子应用作为 子应用组件 激活,另外一部分作为 集中式路由 激活。
  • 一部分子应用既可以作为 集中式路由 激活,也可以作为 子应用组件 激活。

添加 loading

通过配置 loadable 配置,可以为「集中式路由」、「子应用」添加 loading 组件,并可以考虑错误、超时、闪烁等情况的出现,从而为用户提供更好的用户体验。该功能的设计与实现参考至 react-loadable,基本功能较为相似。

function Loading() {
  return <div>Loading...</div>;
}

function App(){
  return <>
    <Table
      loadable={{
        loading: Loading,
      }}
    />
  <>
}

增加错误状态

当微前端子应用加载失败或渲染失败时,loading component 将会接收 error 参数(若没有错误时 error 是 null)

function Loading({ error }) {
  if (error) {
    return <div>Error msg {error?.message}</div>;
  } else {
    return <div>Loading...</div>;
  }
}

避免 loading 闪退

有时 loading 组件的显示时间可能小于 200ms,这个时候会出现 loading 组件闪退的情况。许多用户研究证明,loading 闪退的情况会导致用户感知加载内容的耗时比实际耗时更长,在 loading 小于 200ms 时,不展示内容,用户会认为它更快。

所以 loading 组件还提供了 pastDelay 参数,超过设置的延迟展示时才会为 true,可以通过 delay 参数设置延迟的时长

function Loading({ error, pastDelay }) {
  if (error) {
    return <div>Error! {error?.message}</div>;
  } else if (pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

delay 的默认值为 200ms,可以通过 loadable 中的 delay 来设置延迟展示的时间

function App(){
  return <>
    <Table
      loadable={{
        loading: Loading,
        delay: 300 // 0.3 seconds
      }}
    />
  <>
}

增加超时状态

有时因为网络的原因,从而导致微前端子应用加载失败,从而导致一直展示 loading 的状态,这对于用户而言非常糟糕,因为他们不知道合适才会获得具体的响应,他们是否需要刷新页面,通过增加超时状态可以很好的解决该问题。

loading 组件在超时时会获得 timeOut 参数,当微前端应用加载超时时将会获得 timeOut 属性值为 true

function Loading({ error, timeOut, pastDelay }) {
  if (error) {
    return <div>Error! {error?.message}</div>;
  } else if (timeOut) {
    return <div>Loading timed out, please refresh the page... </div>;
  } else if (pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

超时状态是关闭的,可以通过在 loadable 中设置 timeout 参数开启

function App(){
  return <>
    <Table
      loadable={{
        loading: Loading,
        timeOut: 10000 // 10s
      }}
    />
  <>
}