页面入口

通过本章节,你可以了解到 Modern.js 中的入口约定,以及如何自定义入口。

什么是入口

入口(Entry)指的是一个页面的起始模块。

在 Modern.js 项目中,每一个入口对应一个独立的页面,也对应一条服务端路由。默认情况下,Modern.js 会基于目录约定来自动确定页面的入口,同时也支持通过配置项来自定义入口。

Modern.js 提供的很多配置项都是以入口为维度进行划分的,比如页面标题、HTML 模板、页面 Meta 信息、是否开启 SSR/SSG、服务端路由规则等。

单入口与多入口

Modern.js 初始化的项目是单入口的(SPA),项目结构如下:

. ├── src │ └── routes │ ├── index.css │ ├── layout.tsx │ └── page.tsx ├── package.json ├── modern.config.ts └── tsconfig.json

在 Modern.js 项目中,你可以很方便的将单入口切换成多入口,直接在项目下执行 pnpm run new,根据提示创建入口即可:

? 请选择你想要的操作 创建工程元素
? 请选择创建元素类型 新建「应用入口」
? 请填写入口名称 new-entry

执行后,Modern.js 会自动生成一个新的入口目录,此时可以看到 src/ 目录变成如下结构:

.
├── myapp    # 原入口
│   └── routes
│       ├── index.css
│       ├── layout.tsx
│       └── page.tsx
└── new-entry # 新入口
    └── routes
        ├── index.css
        ├── layout.tsx
        └── page.tsx

原本的入口代码被移动到了和 package.jsonname 同名的目录下,并创建了 new-entry 入口目录。

你可以执行 pnpm run dev 启动开发服务,此时可以看到新增了一条名为 /new-entry 的路由,并且原有页面的路由并未发生变化。

TIP

Modern.js 会将与 package.json 文件中 name 字段同名的入口作为主入口,主入口的路由为 /,其他入口的路由为 /{entryName}

比如,package.json 中的 namemyapp 时,src/myapp 会作为项目的主入口。

入口类型

不同的入口类型具有不同的编译和运行时行为。

默认情况下,Modern.js 启动项目前会对 src/ 下的文件进行扫描,识别入口,并生成对应的服务端路由。

TIP

并非 src/ 下所有的一级目录都会成为项目入口, 入口所在目录必须满足以下四个条件之一:

  1. 具有 routes/ 目录。
  2. 具有 App.[jt]sx? 文件。
  3. 具有 index.[jt]sx? 文件。
  4. 具有 pages/ 目录(兼容 Modern.js 1.0)。

src/ 目录满足入口特征时,Modern.js 会认为当前项目为单入口应用。

TIP

在单入口应用中,默认的入口名为 main

当项目不是单入口应用时,Modern.js 会进一步查看 src/ 下的一级目录。

框架模式入口

框架模式指的是需要使用 Modern.js 的框架能力,例如嵌套路由、SSR、一体化调用等。这类入口约定下,开发者定义的入口并不是真正的编译入口。Modern.js 在启动时会生成一个封装过的入口,可以在 node_modules/.modern/[entryName]/index.js 找到真正的入口。

约定式路由

如果入口中存在 routes/ 目录,Modern.js 会在启动时扫描 routes/ 下的文件,基于文件约定,自动生成客户端路由(react-router)。例如:

.
├── src
│   └── routes
│       ├── layout.tsx
│       └── page.tsx

上述目录中,layout.tsx 中导出的组件会作为最外层的组件,page.tsx 中导出的组件会作为 / 路由的组件。

详细内容可以参考路由方案

自控式路由

如果入口中存在 App.[jt]sx? 文件,开发者可以在这个文件中通过代码的方式,设置客户端路由,或者不设置客户端路由。

import { BrowserRouter, Route, Routes } from '@modern-js/runtime/router';

export default () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route index element={<div>index</div>} />
        <Route path="about" element={<div>about</div>} />
      </Routes>
    </BrowserRouter>
  );
};

详细内容可以参考路由方案

自定义 Bootstrap

如果入口中存在 index.[jt]sx 文件,并且当文件默认导出函数时,Modern.js 会将默认的 bootstrap 函数作为入参传入,并用导出的函数替代默认的 bootstrap,这样开发者可以自定义将组件挂载到 DOM 节点上,或在挂载前添加自定义行为。例如:

export default (App: React.ComponentType, bootstrap: () => void) => {
  // do something before bootstrap...
  initSomething().then(() => {
    bootstrap();
  });
};

此时,Modern.js 生成的文件内容如下:

import React from 'react';
import ReactDOM from 'react-dom/client';
import customBootstrap from '@_modern_js_src/index.tsx';
import App from '@_modern_js_src/App';
import { router, state } from '@modern-js/runtime/plugins';

const IS_BROWSER = typeof window !== 'undefined' && window.name !== 'nodejs';
const MOUNT_ID = 'root';

let AppWrapper = null;
let root = null;

function render() {
  AppWrapper = createApp({
    // runtime 的插件参数...
  })(App);
  if (IS_BROWSER) {
    customBootstrap(AppWrapper, () =>
      bootstrap(AppWrapper, MOUNT_ID, root, ReactDOM),
    );
  }
  return AppWrapper;
}

AppWrapper = render();

export default AppWrapper;

构建模式入口

构建模式指的是不使用 Modern.js 自动生成的入口,而是完全由开发者自行定义页面的入口。

当入口目录中存在 index.[jt]sx,并且没有通过 export default 导出函数时,这类文件就会被识别为 webpack 或 Rspack 的 entry 模块。

此时 Modern.js 不会自动生成入口代码,因此需要你自行将组件挂载到 DOM 节点上,例如:

src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

这种方式等价于开启 Modern.js 的 source.entries.disableMount 选项。当你使用这种方式时,将无法使用 Modern.js 框架的运行时能力,比如 modern.config.js 文件中的 runtime 配置将不会再生效。

自定义入口

在某些情况下,你可能需要自定义入口配置,而不是使用 Modern.js 提供的入口约定。

比如你需要将一个非 Modern.js 项目迁移到 Modern.js,它并不是按照 Modern.js 的目录结构来搭建的。如果你要将它改成 Modern.js 约定的目录结构,可能会存在一定的迁移成本。这种情况下,你就可以使用自定义入口。

Modern.js 提供了以下配置项,你可以在 modern.config.ts 中配置它们:

  • source.entries:用于设置自定义的入口对象。
  • source.disableDefaultEntries:用于关闭 Modern.js 默认的入口扫描行为。当你使用自定义入口时,项目的部分结构可能会恰巧命中 Modern.js 约定的目录结构,但你可能不希望 Modern.js 为你自动生成入口配置,开启该选项可以避免这个问题。

示例

下面是一个自定义入口的例子,你也可以查看相关配置项的文档来了解更多用法。

modern.config.ts
export default defineConfig({
  source: {
    entries: {
      // 指定一个名为 'my-entry' 的入口
      'my-entry': {
        // 入口模块的路径
        entry: './src/my-page/index.tsx',
        // 关闭 Modern.js 自动生成入口代码的行为
        disableMount: true,
      },
    },
    // 禁用入口扫描行为
    disableDefaultEntries: true,
  },
});

注意,当你开启 disableMount 时,将无法使用 Modern.js 框架的运行时能力,比如 modern.config.ts 文件中的 runtime 配置将不会再生效。