---
sidebar_position: 2
---

# 开发移动页面

本章将介绍如何使用 Modern.js，进行移动页面的开发。本章对应的代码仓库地址在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/quick-start/mobile-pages)。

通过本章你可以了解到：

- 如何创建一个移动端网页项目。
- 如何为项目创建新入口。
- 如何使用 Modern.js 配置选项。
- 如何使用组件样式。
- 如何开发和使用 BFF API。
- 如何配置项目所需的静态资源、自定义 HTML。
- 如何使用测试功能。

## 环境准备

import EnvPrepare from '@site/docs/components/env-prepare.md';

<EnvPrepare/>

## 创建项目

使用 `@modern-js/create` 创建新项目，运行命令如下：

```bash
npx @modern-js/create mobile-pages
```

:::info 注
`mobile-pages` 为创建的项目名。
:::

按照如下选择，生成项目：

```bash
? 请选择你想创建的工程类型： 应用
? 请选择开发语言： TS
? 请选择包管理工具： pnpm
? 是否需要支持以下类型应用： 不需要
? 是否需要调整默认配置： 否
```

## 开发调试

进入项目根目录， 之后执行 `pnpm run dev` 即可启动开发服务器：

```bash
# 进入项目根目录
cd mobile-pages

# 启动开发服务器
pnpm run dev
```

浏览器中访问 `http://localhost:8080`，可以看到应用已经正常启动。

修改 `src/pages/index.tsx` 会触发重新编译和热更新，浏览器中页面会自动展示对应变化。

### Unbundled 开发模式

import DevUnbundle from '@site/docs/components/dev-unbundle.md'

<DevUnbundle/>

:::info 注
Unbundled 模式暂不支持在 Windows 平台使用，支持即将上线。
:::

### IDE 支持

import DevIDE from '@site/docs/components/dev-ide.md'

<DevIDE/>

## 创建入口

在 Modern.js 中，一个[入口](/docs/guides/tutorials/c07-app-entry/7.1-intro)，经过构建后会生成一个对应的 HTML 文件。默认生成的项目只包含一个入口，而一个移动项目通常包含多个独立页面，现在我们创建一个新入口。

在项目根目录下，执行 `pnpm run new`，进行如下选择：

```bash
? 请选择你想要的操作： 创建工程元素
? 创建工程元素： 新建「应用入口」
? 填写入口名称： activity
? 是否修改默认的应用入口配置：否
```

创建完成，项目的 `src/` 目录结构如下：

```md
.
├── src/
│   ├── activity/
│   │   └── App.tsx
│   ├── mobile-pages/
│   │   └── App.tsx
│   ├── .eslintrc.json
```

其中， `activity/` 目录对应新建的入口，项目默认的入口（主入口）代码被移动到 `mobile-pages/` 目录下。

:::info 注
使用生成器将应用从单入口转换成多入口时，原本主入口的代码将会被移动到与当前应用 `package.json` 同名的目录下。
:::

重新启动应用，控制台会输出不同入口对应的访问地址。默认情况下，主入口对应的访问地址为 **{域名根路径}**，其他入口对应的访问地址为 **{域名根路径}/{入口名称}**，如下所示：

```bash
App running at:

  > Local:
    activity        http://localhost:8080/activity
    mobile-pages    http://localhost:8080/
```

:::info 补充信息
更多信息，请参考【[添加应用入口](/docs/guides/tutorials/c07-app-entry/7.2-add-entry-in-cli)】。
:::

## 关闭客户端路由和状态管理

默认生成的项目是开启客户端路由和状态管理功能的，而一个移动项目一般都是由多个相对简单的独立页面组成，不涉及客户端路由和复杂的状态管理，此时我们可以关闭客户端路由和状态管理功能，以减少项目打包后的体积大小，提高页面性能。

我们打开项目根路径下的 `modern.config.js`，目前应该是这样的：

```js title="modern.config.js"
export default defineConfig({
  runtime: {
    state: true,
    router: true,
  },
});
```

然后我们将 `state` 和 `router` 这两个配置项设置为 `false`，就关闭了客户端路由和状态管理功能。

:::info 补充信息
更多用法，请参考【[`state`](/docs/apis/config/runtime/state)】、【[`router`](/docs/apis/config/runtime/router)】。
:::

## 组件样式

### Utility Class

Utility Class 是对通用功能（如文字居中、水平对齐等）涉及的 CSS 规则的封装。开发者可以直接使用这些 CSS Class 进行样式设置。

Modern.js 集成了主流的 Utility Class 解决方案 —— [Tailwind CSS](https://tailwindcss.com/)，我们利用 [Tailwind CSS](https://tailwindcss.com/)
设置组件的通用样式。

首先，需要开启 Tailwind CSS 支持，在项目根目录下执行 `pnpm run new`，进行如下选择：

```bash
? 请选择你想要的操作: 启用可选功能
? 启用可选功能: 启用 Tailwind CSS 支持
```

然后在入口的根组件，这里我们以主入口为例，在 `src/mobile-pages/App.tsx` 添加如下代码：

```js title="src/mobile-pages/App.tsx"
import 'tailwindcss/base.css';
import 'tailwindcss/components.css';
import 'tailwindcss/utilities.css';
```

这样我们就可以在主入口下的组件中使用 Tailwind CSS 提供的 Utility Class 了。我们使用 `text-center` 这个 Class，为 `App.tsx` 中的文字设置居中效果：

```js title="src/mobile-pages/App.tsx"
const App: React.FC = () => (
  <div className="text-center">Hello, Modern.js!</div>
);
```

浏览器访问 `http://localhost:8080`，会发现 `Hello, Modern.js!` 已经居中显示了。

:::info 补充信息
关于 Tailwind CSS 使用的更多介绍，请参考【[Tailwind CSS](/docs/guides/usages/css/tailwindcss)】。
:::

### CSS in JS

组件有时候需要设置自己的特有样式，这时候使用 Utility Class 就不是很方便了。我们可以用 CSS in JS 的写法直接在组件的 JS 代码中编写特有样式。Modern.js 通过集成 [styled-components](https://styled-components.com/) 提供 CSS in JS 的支持。

接下来，我们使用 CSS in JS 实现 `activity` 入口组件样式。

首先，我们在 `src/activity/App.tsx` 里修改顶部的代码，引入 `styled` 模块：

```js
import styled from '@modern-js/runtime/styled';
```

`styled` 模块的使用方式同 [styled-components](https://styled-components.com/) 一致，我们可以定义如下用于给页面标题添加样式的组件：

```js
const TitleWrapper = styled.div`
  font-size: 2rem;
  text-align: center;
  margin: 0.5rem 0;
  color: goldenrod;
`;
```

此时，`src/activity/App.tsx` 的完整代码如下：

```js title="src/activity/App.tsx"
import styled from '@modern-js/runtime/styled';

const TitleWrapper = styled.div`
  font-size: 2rem;
  text-align: center;
  margin: 0.5rem 0;
  color: goldenrod;
`;
const App: React.FC = () => {
  return (
    <div>
      <TitleWrapper>Promotion Campaign</TitleWrapper>
    </div>
  );
};

export default App;
```

:::info 注
上面的示例也可以通过 Tailwind CSS 提供的 Utility Class 实现，这里仅用于举例说明 CSS in JS 的使用方式。通常，Utility Class + CSS in JS 的方案可以满足绝大多数项目的样式编写需求。
:::

:::info 补充信息
关于组件样式的更多用法，请参考【[CSS 开发方案](/docs/guides/usages/css/css-in-js)】。
:::

## 客户端兼容性

Modern.js 提供自动 Polyfill、Browserslist 配置、差异化分发等特性，能够最大化的兼顾客户端兼容性和资源加载性能。关于这部分内容的更多介绍，请参考【[客户端兼容性](/docs/guides/usages/basic-configuration/compatibility)】。

## 一体化 BFF

现在 `activity` 入口页面还没有数据，我们可以通过服务端 API 动态获取数据（商品列表数据）。服务端 API 地址为：<https://lf3-static.bytednsdoc.com/obj/eden-cn/beeh7uvzhq/products.json>。

但这个 API 并不是专门为当前项目提供的，部署也是在一个独立的域名下。

通常情况下，项目需要创建一个和当前项目部署在同一域名下的专属 API，并在这个 API 内部去调用原始数据获取，并进行裁剪聚合等。在前端，这样的需求通常使用 BFF 层来实现。

Modern.js 提供了开箱即用的 BFF 能力，支持和前端代码共同开发、调试、部署。

:::info 注
如果已经具备了为前端项目专门开发的、部署在同域下的 API，则不需要再创建 BFF 层，前端代码直接调用 API 即可。
:::

我们执行 `pnpm run new` 来开启 BFF 功能：

import LaunchBFFChoices from '@site/docs/components/launch-bff-choices.md';

<LaunchBFFChoices />

执行完成后，项目中新增了 `api/` 目录，添加 `api/products.ts` 文件，实现对数据获取 API 的调用（需要先安装 axios 依赖）：

```js
import axios from 'axios';

export default async (): Promise<
  { id: string, name: string, price: number }[],
> => {
  const res = await axios.get(
    'https://lf3-static.bytednsdoc.com/obj/eden-cn/beeh7uvzhq/products.json',
  );
  return res.data;
};
```

重新执行 `dev` 命令，我们已经可以访问 `http://localhost:8080/api/products`，并成功获取推荐的商品数据。

接下来修改 `src/activity/App.tsx`，调用 BFF API 来获取数据。

通常情况下，组件代码中通过 `axios`，执行请求地址来获取数据。但是 Modern.js 提供了一种更加简洁的方式，可以像使用函数一样来调用 API，关键代码如下：

```js title="src/activity/App.tsx"
import React, {useEffect, useState} from 'react';
import products from '@api/products';

const App: React.FC = () => {

  const [data, setData] = useState([]);

  useEffect(() => {
    const load = async () => {
      const _data = await products();
      setData(_data);
    };

    load();
  }, []);

  // ...
```

我们从 `@api/products` 路径下导入 `products`，然后直接把 `products` 作为函数调用，即等价于通过 `axios` 调用 API 获取数据的效果，这就是一体化 BFF。

渲染商品列表数据的关键代码如下：

```js title="src/activity/App.tsx"
// ...

const App: React.FC = () => {
  const [data, setData] = useState([]);

  // ...

  return (
    <div>
      <TitleWrapper>Promotion Campaign</TitleWrapper>
      <div>
        {data.map(item => {
          const { id, name, price } = item;
          return (
            <ItemWrapper key={id}>
              <span>{name}</span>
              <span>${price}</span>
            </ItemWrapper>
          );
        })}
      </div>
    </div>
  );
};

export default App;
```

:::info 补充信息
利用一体化 BFF，我们可以实现以往需要专门的服务端项目支持才能实现的需求，如集成微信 JS SDK 时的权限验证逻辑。更多信息请参考【[一体化 BFF](/docs/guides/features/server-side/bff/function)】。
:::

## 配置 Favicon 等通用静态资源

在 `config/` 目录下，添加 `favicon.ico` 图标文件，构建生成的 HTML 页面将包含 favicon 信息。

在 `config/public` 目录下，可以放置任意格式的静态资源化文件，文件会被 Serve 在应用同域名下。

在微信网页开发时，[绑定 JS 接口安全域名](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#2)需要上传一个微信提供的 txt 文件到自己的 Web 服务器，并保证文件可以被正常访问。

我们可以把这个文件放到 `config/public` 目录，然后访问 **{域名}/{文件名}**就可以获取文件内容。例如，我们在该目录下新建 `MP_verify_abcdef.txt` 文件，重新启动应用，访问 `http://localhost:8080/MP_verify_abcdef.txt` 即可获取文件内容。

:::info 补充信息
关于静态资源的更多使用方法，请参考配置 【[`config/public`](/docs/apis/hooks/mwa/config/public)】。
:::

## 自定义 HTML

Modern.js 支持修改默认使用的 HTML 模板文件。

例如，当我们需要集成 Google Analytics 等工具，统计分析用户的行为信息。我们可以通过自定义项目使用的 HTML 模板，在模板中添加相关工具的 JS SDK 引用代码，实现集成。

以 Google Analytics 为例，我们新建 `config/html/top.html` 文件，内容如下：

```js
<!-- Global Site Tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'GA_MEASUREMENT_ID');
</script>
```

在构建生成的 HTML 页面 `<head>` 标签内，将包含上面的代码，如下图所示：

![custom-html](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/start/custom-html.png)

:::info 补充信息
更多信息，请参考【[自定义 HTML 模板](/docs/guides/usages/basic-configuration/html)】。
:::

## 测试

Modern.js 内置 [Jest](https://jestjs.io/) 、[Testing Library](https://testing-library.com/) 等测试库/框架，提供单元测试、组件/页面集成测试、业务模型 Model 测试等功能。

使用测试功能，需要先开启该功能。在项目根目录下，执行 `pnpm run new`，进行如下选择：

```bash
? 请选择你想要的操作： 启用可选功能
? 启用可选功能： 启用「单元测试 / 集成测试」功能
```

安装依赖后，可以看到 package.json 中新增了一项依赖：

```bash
+ @modern-js/plugin-testing
```

这一插件提供了 Testing API 用于页面集成测试，我们以主入口为例，演示页面的集成测试。

新建 `src/mobile-pages/__tests__/` 目录，用于放置测试用例，并编写测试用例 `App.test.tsx`：

```js title="App.test.tsx" {1,6-7}
import { renderApp } from '@modern-js/runtime/testing';
import App from '../App';

describe('main entry', () => {
  it('should have contents', () => {
    const { getByText } = renderApp(<App />);
    expect(getByText('Hello, Modern.js !')).toBeInTheDocument();
  });
});
```

默认情况下，`src/` 目录下文件名匹配规则 `*.test.(t|j)sx?` 的文件都会被识别为测试用例。

执行 `pnpm run test`，会运行项目下的所有测试用例。

:::info 补充信息
更多用法，请参考【[Testing API](/docs/apis/runtime/testing/render)】。
:::

## 部署

import Deploy from '@site/docs/components/deploy.md';

<Deploy/>
