UmiJS-企业级-React-开发-全-

UmiJS 企业级 React 开发(全)

原文:zh.annas-archive.org/md5/99b385c2f9a7f623ba34ab371d4b8005

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

UmiJS 是一个可扩展的 JavaScript 框架,用于构建企业级前端应用程序。Umi 使用 React,并基于一个路由系统,允许你创建快速和响应式的应用程序。

在这本书中,我们将构建一个用于客户关系管理(CRM)系统的前端网络应用程序。从你的环境设置开始,我将向你介绍 UmiJS 的主要特性和项目结构。之后,我们将探索 Ant Design,这是一个拥有大量 React 组件库的设计系统,可以快速构建与现代 Umi 深度集成的现代和响应式用户界面。

你还将学习一种基于模型和服务的处理 HTTP 请求和响应的方法,以及在复杂场景中控制应用程序的状态。

在学习如何使用 Umi 之后,你将探索如何通过实施一致的代码风格和使用格式化工具如 Prettier 和 EditorConfig 来提高代码质量。你还将学习如何为前端应用程序设计和实现测试。

最后,你将在 AWS Amplify 上托管你的 CRM 前端应用程序,AWS Amplify 是一个现成的平台,供前端开发者使用多种 AWS 服务构建全栈应用程序。

本书面向的对象

这本书是为那些刚开始使用 UmiJS 并构建大型网络应用程序的 React 开发者而写的。我假设你已经了解 React 和设计网络应用程序的基础知识。

本书涵盖的内容

第一章环境设置和 UmiJS 简介,你将安装所有需要的工具来跟随本书的练习,并学习 UmiJS 的主要特性。

第二章使用 Ant Design 创建用户界面,你将探索 Ant Design 系统,并使用其 React 组件库创建界面。

第三章使用模型、服务和模拟数据,你将学习一种基于模型和服务的处理请求、管理应用程序状态和通过模拟文件模拟数据的方法。

第四章错误处理、身份验证和路由保护,你将在应用程序中实现错误处理、安全控制和授权。

第五章代码风格和格式化工具,我们将讨论代码风格并配置 Prettier 和 EditorConfig 以在项目中自动格式化和强制执行一致的代码风格。

第六章测试前端应用程序,我们将讨论软件测试,并使用 Puppeteer 为你的应用程序实现一些测试。

第七章,“单页应用部署”,是你准备应用程序进行部署并在 AWS Amplify 上托管的地方。

要充分利用本书

要完成这些书籍练习,你只需要一台装有现代操作系统的电脑(例如 Windows 10/11、macOS 10.15 或 Ubuntu 20.04)。我将在第一章,“环境设置和 UmiJS 简介”中给你安装其他所需软件的说明。

需要指出的是,你需要一个免费的 GitHub 账户来访问代码示例并完成第七章,“单页应用部署”。

如果你使用的是这本书的数字版,我们建议你亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助你避免与代码复制粘贴相关的任何潜在错误。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/Enterprise-React-Development-with-UmiJs。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在这个例子中,我们使用了describe方法为与数学问题相关的两个测试创建了一个组。”

代码块设置如下:

export default {
  'home.recents': 'Recent opportunities',
  'greetings.hello': 'Hello',
  'greetings.welcome': 'welcome',
};

当我们希望将你的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:

async function login(page: Page) {
  await page.goto('http://localhost:8000');
  await page.waitForNavigation();
  await page.type('#username', 'john@doe.com');
  await page.type('#password', 'user');
  await page.click('#loginbtn');
}

任何命令行输入或输出都应如下编写:

yarn add -D puppeteer

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“机会页面允许用户浏览并注册新的销售机会。”

小贴士或重要注意事项

看起来像这样。

联系我们

我们始终欢迎读者的反馈。

customercare@packtpub.com并在邮件主题中提及书名。

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将非常感激你向我们报告。请访问www.packtpub.com/support/errata并填写表格。

copyright@packt.com,并附有材料链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且对撰写或参与一本书籍感兴趣,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《使用 UmiJS 进行企业级 React 开发》,我们非常期待听到您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区都非常重要,并将帮助我们确保我们提供高质量的内容。

第一部分:配置 UmiJS 和创建用户界面

本节旨在向读者介绍 UmiJS 并通过实际示例解释其主要功能。在本节中,读者将从零开始创建一个 Umi 项目,构建界面,并通过实现服务和模型来管理应用程序状态。

本节包括以下章节:

  • 第一章, 环境配置和 UmiJS 简介

  • 第二章, 使用 Ant Design 创建用户界面

  • 第三章, 使用模型、服务和模拟数据

第一章:环境搭建和 UmiJS 简介

UmiJS 是蚂蚁金服的底层前端框架,也是一个用于开发企业级前端应用的开源项目。它是一个强大的框架,你可以将其与 Ant Design 结合使用,提供构建现代用户体验所需的一切。

在本章中,你将学习如何使用 UmiJS 和Visual Studio CodeVSCode)安装和配置项目。你还将了解 UmiJS 的文件夹结构和主要文件。然后,你将学习如何使用u****mi history进行快速页面导航,最后发现Umi UI,这是一个与 UmiJS 交互并添加组件到项目的可视化选项。

我们将涵盖以下主要主题:

  • 设置我们的环境和配置 UmiJS

  • 理解 UmiJS 的文件夹结构和其主要文件

  • 探索 Umi CLI 和添加页面

  • 理解 UmiJS 中的路由和导航

  • 使用 Umi UI

到本章结束时,你将学会所有开始开发项目所需的知识,你还将了解 UmiJS 项目及其配置的基本行为。

技术要求

要完成本章的练习,你只需要一台安装了任何操作系统的电脑(我推荐使用 Ubuntu 20.04 或更高版本)。

你可以在以下链接提供的 GitHub 仓库的Chapter01文件夹中找到完整的项目:

github.com/PacktPublishing/Enterprise-React-Development-with-UmiJs

设置我们的环境和配置 UmiJS

在本节中,我们将安装和配置 VSCode、EditorConfig 扩展和 Prettier 扩展,并创建我们的第一个 UmiJS 项目。

让我们从安装一个源代码编辑器开始。你可以使用任何支持 JavaScript 和 TypeScript 的编辑器,但在这本书中,我将广泛使用 VSCode。它是一个免费的编辑器,具有集成的终端和内置的 Git 控制,原生支持 JavaScript、TypeScript、Node.js 以及许多其他语言的扩展。

VSCode 可以作为 Snap 包提供,你可以在 Ubuntu 上通过运行以下命令来安装它:

$ sudo snap install code ––classic

对于 Mac 用户,你可以在 macOS 上使用 Homebrew 安装它,通过运行以下命令:

$ brew install --cask visual-studio-code

如果你正在 Windows 上使用 Chocolatey,你可以运行以下命令:

> choco install vscode

或者,你可以下载位于code.visualstudio.com/的安装程序。

重要提示

您可以在 brew.sh/ 上找到有关在 macOS 上安装 Homebrew 的说明,以及在 chocolatey.org/install 上安装 Chocolatey 的说明。如果您是 Windows 用户,您可以在 Windows Subsystem for LinuxWSL) 中安装 Ubuntu,并使用常见的 Linux 命令设置您的项目。您可以在 docs.microsoft.com/en-us/windows/wsl/install 上了解更多关于 WSL 的信息。

接下来,我们需要安装使用 UmiJS 进行开发所需的依赖。首先,让我们在终端中输入并运行以下命令来安装 Node.js:

$ sudo apt update
$ sudo apt install nodejs -y

第一个命令更新了镜像,第二个命令使用 -y 标志安装 Node.js,这会跳过用户确认步骤来安装。

您可以使用 Homebrew 在 macOS 上运行以下命令来安装 Node.js:

$ brew install node

如果您在 Windows 上使用 Chocolatey,您可以运行以下命令:

> choco install nodejs

或者,您可以从 nodejs.org/en/ 下载可用的安装程序。

Node.js 有一个名为 npm 的默认包管理器,但在这本书中,我们将广泛使用 Yarn 而不是 npm,所以我建议您安装它。您可以在终端中运行以下命令来完成此操作:

$ npm install -g yarn

此命令将在您的系统中全局安装 Yarn。

这样,我们就准备好开始使用 UmiJS 了。但首先,让我们更深入地了解 UmiJS 以及它可以解决哪些问题。

UmiJS 简介及创建您的第一个项目

UmiJS 是一个用于开发企业级前端应用的框架。这意味着 Umi 提供了一套工具,用于解决在构建需要提供现代用户体验且易于维护和修改的大型商业应用时遇到的日常问题。

使用 Umi,您可以利用 Umi 与 Ant Design 的深度集成,快速开发具有国际化、权限和美观界面的应用程序。

Umi 的另一个显著优势是,您可以根据需要添加各种已发布的插件到您的项目中。您还可以通过开发自己的插件来扩展它,以满足特定的解决方案。

现在您对 Umi 了解更多了,让我们按照以下步骤创建您的第一个项目:

  1. 为项目创建一个新的文件夹并在终端中打开它:

    $ mkdir umi-app; cd umi-app
    
  2. 使用 umi-app 模板创建一个新的项目:

    $ yarn create @umijs/umi-app
    
  3. 通过运行以下命令安装项目依赖:

    $ yarn
    
  4. 通过运行以下命令启动项目:

    $ yarn start
    

现在我们已经设置好了项目!您可以在浏览器中输入 http://localhost:8000 来打开它并查看结果。

让我们通过添加代码格式化来简化我们的工作,进行最后的配置。

安装 EditorConfig 和 Prettier 扩展

UmiJS 在 umi-app 模板中默认提供的一个工具是 EditorConfig,这是一个编辑器读取以定义跨 IDE 和文本编辑器的代码格式的文件格式。你将在 第五章 中了解更多关于代码风格的信息,代码风格和格式化工具。一些编辑器和 IDE 提供了对 EditorConfig 的原生支持,而在其他情况下,例如 VSCode,你需要安装一个插件,所以让我们按照以下步骤安装它:

  1. 打开 VSCode 并按 Ctrl + P。此快捷键将在顶部打开以下字段:

![图 1.1 – VSCode 快速打开图片 1.01

图 1.1 – VSCode 快速打开

  1. 输入以下命令并按 Enter 安装对 EditorConfig 的官方扩展:

    ext install EditorConfig.EditorConfig 
    

umi-app 模板预装了 Prettier,并已预配置用于格式化代码。你可以通过运行 yarn prettier 命令来使用它。然而,更好的选择是在保存更改或粘贴代码块时让 VSCode 为你格式化。

为了做到这一点,我们需要安装 Prettier 扩展并将其配置为默认的代码格式化程序。要安装和配置 Prettier 扩展,请按照以下步骤操作:

  1. Ctrl + P 并输入以下命令,然后按 Enter 安装 Prettier 的官方扩展:

    ext install esbenp.prettier-vscode
    
  2. 接下来,按 Ctrl + , 打开 VSCode 预设,并在搜索字段中输入 formatter 并按 Enter

  3. 编辑器:默认格式化程序 下,选择 Prettier - 代码格式化程序

  4. 检查 编辑器:粘贴时格式化编辑器:保存时格式化 选项,如下面的截图所示:

![图 1.2 – VSCode 编辑器配置图片 1.02

图 1.2 – VSCode 编辑器配置

在本节中,我们学习了如何配置我们的环境,更深入地了解了 UmiJS,并创建了我们的第一个项目。现在,让我们更仔细地看看项目结构。

理解 UmiJS 文件夹结构和其主要文件

在本节中,你将了解 UmiJS 的文件夹结构,并将添加一些必要的配置到文件和文件夹中。

基于的 umi-app 模板创建的项目生成了一系列文件夹,分别负责项目的不同部分。让我们看看每个文件夹的作用:

  • mock: 在这个文件夹中,我们存储我们的模拟端点定义以生成我们可以与之交互的模拟 API。

  • src: 这是我们的所有组件所在的源文件夹。

  • src/.umi: 这个文件夹是由 UmiJS 在每次项目编译时自动生成的,其中包含其内部配置。

  • src/pages: 负责根据配置的路由渲染页面的 React 组件位于此文件夹中。

这些是 umi-app 模板中包含的文件夹,但在 UmiJS 项目中还有其他一些重要的文件夹,所以让我们添加它们。

我们首先添加的文件夹是 config

添加配置和地区文件夹

在我们项目的根文件夹中,有一个名为 .umirc.ts 的文件。此文件包含 Umi 及其插件的配置。当您的项目紧凑时,这是一个不错的选择,但随着项目的增长和复杂化,配置文件可能变得难以维护。为了避免这种情况,我们可以将配置拆分为位于 config 文件夹中的不同部分。现在,让我们通过在 VSCode 中打开您的项目并按照以下步骤进行操作来完成此操作:

  1. 在您项目的根目录中,创建一个名为 config 的新文件夹。

您可以通过点击文件夹列表上方的右上角图标来完成此操作。

图 1.3 – VSCode 新文件夹图标

图 1.3 – VSCode 新文件夹图标

  1. .umirc.ts 文件移动到 config 文件夹,并将其重命名为 config.ts

您可以通过选择文件并按 F2 键来重命名文件。

  1. config 文件夹中,创建一个名为 routes.ts 的新文件。在此文件中,我们将配置应用程序的路由。

您可以通过点击文件夹列表右上角的图标来完成此操作。

图 1.4 – VSCode 新文件图标

图 1.4 – VSCode 新文件图标

  1. 将此代码粘贴到 routes.ts 文件中并保存:

    export default [
      {
        path: '/',
        component: '@/pages/index',
      },
    ];
    

此代码定义了渲染位于 pages 文件夹中的组件索引的根路径('/')。

  1. 现在,我们可以将 routes.ts 文件导入到 config.ts 中,并在 config.ts 文件中添加以下行:

    import routes from './routes';
    

然后,我们可以重写路由部分,如下所示:

import { defineConfig } from 'umi';
import routes from './routes';
export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes,
  fastRefresh: {},
});

Umi 还支持在 src 文件夹中的 locales,并在 config 文件夹下的 config.ts 文件中添加以下配置:

config.ts

import { defineConfig } from 'umi';
import routes from './routes';
export default defineConfig({
  locale: {
    default: 'en-US',
    antd: true,
    baseNavigator: true,
    baseSeparator: '-',
  },
  nodeModulesTransform: {
    type: 'none',
  },
  routes,
  fastRefresh: {},
});

locale 配置属性如下:

  • default:默认应用程序语言。

  • antd:启用 Ant Design 组件国际化。

  • baseNavigator:启用浏览器语言检测。

  • baseSeparator:在 src/locales 文件夹下本地化的多语言文件中使用的分隔符。

现在,我们可以通过在 locales 文件夹中添加多语言文件来支持国际化。例如,要支持英语,我们需要添加一个名为 en-US.js 的文件。

现在,我们将添加 app.tsx 文件来设置运行时配置。

运行时配置

Umi 使用名为 app.tsx 的文件在运行时扩展您的应用程序配置。此文件非常有用,可以使用 app.tsx 文件配置初始状态,该文件需要位于 src 文件夹中。

按照之前演示的步骤,在 src 文件夹中添加一个名为 app.tsx 的文件。

到目前为止,我们的项目结构应该看起来像这样:

图 1.5 – 最后修改后的项目结构

图 1.5 – 最后修改后的项目结构

通过即将到来的章节中的练习,您将更好地理解所有这些功能。

现在您已经了解了 Umi 项目结构并添加了缺失的文件夹和文件,让我们来学习一些 Umi 命令行界面CLI)中的有用命令。

探索 Umi CLI 和添加页面

在本节中,我们将探索 Umi CLI 以自动化任务,并使用 generate 命令向您的项目添加一些页面。

Umi 提供了一个 CLI,包含构建、调试、列出配置等命令。您可以使用它们来自动化任务。其中一些命令已经在 umi-app 模板中配置为 package.json 文件中的脚本:yarn start 将执行 umi devyarn build 将执行 umi build,等等。

这些是可用的主要命令:

  • umi dev:编译应用程序并启动开发服务器以进行调试。

  • umi build:在 dist 文件夹中编译应用程序包。

  • umi webpack:这显示了 Umi 生成的 webpack 配置文件。

  • umi plugin list:列出所有正在使用的 Umi 插件。

  • umi generate page:创建一个新的页面模板。

    重要提示

    对于更多命令,请参阅在 umijs.org/docs/cli 可用的文档。

让我们使用 generate page Umi CLI 命令添加一些页面。按照以下步骤操作:

  1. 首先,删除 src/pages 文件夹下的文件,然后运行以下命令添加两个页面:

    $ yarn umi g page /Home/index ––typescript ––less
    $ yarn umi g page /Login/index ––typescript ––less
    

这些命令在 pages 文件夹下生成两个组件,LoginHome,支持 TypeScript 和 Less。

  1. 要访问这些页面,我们需要定义路由,因此修改您的 routes.ts 文件以定义为新路由创建的组件:

routes.ts

export default [
  {
    path: '/',
    component: '@/pages/Login',
  },
  {
    path: '/home',
    component: '@/pages/Home',
  },
];
  1. 要检查结果,通过运行 yarn start 启动项目,然后导航到 http://localhost:8000/;您应该看到登录页面。

  2. 导航到 http://localhost:8000/home;您现在应该看到主页。

现在我们已经设置了页面,我们可以通过使用 umi history 来了解更多关于 Umi 路由和导航的信息。

理解 UmiJS 中的路由和导航

在本节中,您将了解 Umi 路由系统和配置路由的选项。您还将学习如何访问路由参数和查询字符串,以及如何在页面之间导航。

一个 Umi 项目是一个 index.html),当我们访问不同地址时看到的所有其他页面都是在这个相同页面上渲染的组件。Umi 执行解析路由和渲染正确组件的工作;我们只需要定义当路由匹配特定路径时,需要渲染哪个组件。如您所注意到的,我们已经那样做了。但还有其他配置选项。例如,我们可以设置子路由来定义各种页面的标准布局:

routes.ts

export default [
  {
    path: '/',
    component: '@/layouts/Header',
    routes: [
      { path: '/login', component: '@/pages/Login' },
      { path: '/home', component: '@/pages/Home' },
    ],
  },
];

上述示例定义了所有位于 '/' 下的路由都将有一个默认的头部,这是一个位于 src/layouts 文件夹中的组件。

头部组件应如下所示:

import React from 'react';
import styles from './index.less';
export default function (props: { children: React.ReactChild }) {
  return (
    <div className={styles.layout}>
      <header className={styles.header}>
        <h1>Umi App</h1>
      </header>
      {props.children}
    </div>
  );
}

当您访问定义的路由时,props.children 将接收组件。

我们还有另一个选项,即重定向路由。考虑以下示例:

routes.ts

export default [
  {
    path: '/',
    redirect: '/app/login',
  },
  {
    path: '/app',
    component: '@/layouts/Header',
    routes: [
      { path: '/app/login', component: '@/pages/Login' },
      { path: '/app/home', component: '@/pages/Home' },
    ],
  },
];

使用此配置,当您访问 http://localhost:8000/ 时,Umi 将立即将页面重定向到 http://localhost:8000/app/login

我们还可以定义路径是否应该是精确的:

{
   exact: false,
   path: '/app/login',
   component: '@/pages/Login',
}

此配置定义了您可以在/app/login下的任何路径访问此页面,例如http://localhost:8000/app/login/user。默认情况下,所有路径都是精确的。

您现在已经了解了路由系统的工作原理以及我们为路由提供的不同配置选项。现在,您将学习如何访问路径和查询字符串参数,以及关于传统路由和页面之间的导航。

理解路径参数和查询字符串

有时候我们需要在路由路径中识别一个资源。想象一下,在我们的项目中有一个只显示产品信息的页面。当访问这个页面时,我们需要指定要获取哪个产品信息。我们可以通过在路由路径中识别产品 ID 来实现这一点:

{
  path: '/product/:id',
  component: '@/pages/Product',
},

如果参数不是访问页面的必需项,您必须添加?字符,如下所示:/product/:id?

要访问产品 ID,我们可以使用 Umi 提供的useParams钩子:

import { useParams } from 'umi';
export default function Page() {
  const { id } = useParams<{ id: string }>();

您也可以在路由后接收查询字符串参数。查询字符串参数是 URL 中?字符序列中的键值对,例如以下示例:/app/home?code=eyJhbGci。在这里,code包含值eyJhbGci

我们没有特定的钩子来访问查询字符串参数值,但我们可以通过 umi 历史记录轻松做到这一点:

import { history } from 'umi';
export default function Page() {
  const { query } = history.location;
  const { code } = query as { code: string };

现在,让我们看看如何在传统路由中定义参数。

传统路由

UmiJS 在pages文件夹下根据您的项目结构提供自动路由配置。如果 UmiJS 在config.ts.umirc.ts文件中找不到路由定义,它将依赖这个配置。

如果您想配置路由参数,可以在[]中命名包含的文件,如下所示:[id].tsx。如果此参数不是访问页面的必需项,您必须添加$字符,如下所示:[id$].tsx

Figure 1.6 – Optional route parameter in conventional routing

Figure 1.06_B18503.jpg

图 1.6 – 传统路由中的可选路由参数

接下来,您将看到如何在不同页面之间导航。

页面之间的导航

当我们需要设置页面之间的导航时,通常我们会使用 DOM 历史对象和锚点标签。在 UmiJS 中,我们有类似的选项进行导航:Link组件。

您可以使用Link组件在页面之间创建超链接,如下例所示:

import { Link } from 'umi';
export default function Page() {
  return (
    <div>
      <Link to="/app/home">Go Home</Link>
    </div>
  );
}

您还可以使用push() umi 历史命令设置页面之间的导航,如下例所示:

import { history } from 'umi';
export default function Page() {
  const goHome = () => {
    history.push('/app/home');
  };
  return (
    <div>
      <button onClick={goHome}></button>
    </div>
  );
}

除了push()命令外,umi 历史记录还有goBack()命令可以回退历史堆栈中的一页,以及goForward()命令可以前进一页。

我们已经涵盖了 Umi 路由系统的所有基本方面,包括配置路由的不同选项、访问路径和查询字符串参数以及页面之间的导航。

在完成这一章之前,我将介绍 Umi 提供的一个令人兴奋的功能,如果您更喜欢以可视化的方式与项目交互。

使用 Umi UI

Umi UI 是 Umi 的视觉扩展,用于与项目交互。您可以通过图形用户界面运行命令来安装依赖项、验证和测试代码、构建项目以及添加组件。

在使用 Umi UI 之前,我们需要添加@umijs/preset-ui包。您可以通过运行以下命令来完成此操作:

$ yarn add @umijs/preset-ui -D 

现在,当您启动项目时,您应该看到以下控制台日志:

![Figure 1.7 – Umi UI 启动日志]

![img/Figure_1.07_B18503.jpg]

Figure 1.7 – Umi UI 启动日志

导航到http://localhost:8000,您会注意到 UmiJS 标志在右下角出现一个气泡。点击此气泡将打开 Umi UI(您也可以通过http://localhost:3000访问 Umi UI)。

![Figure 1.8 – Umi UI 右下角气泡]

![img/Figure_1.08_B18503.jpg]

Figure 1.8 – Umi UI 右下角气泡

让我们看看使用 Umi UI 能做什么,从任务开始:

  • dist文件夹。您也可以点击ENVS来选择编译选项,例如 CSS 压缩。

  • 使用lint脚本来使用此选项。

  • 测试:此选项将测试项目。您需要先编写测试。

  • 安装:此选项将安装所有项目依赖项。

以下截图显示了 Umi UI 任务标签页:

![Figure 1.9 – Umi UI Task tab]

![img/Figure_1.09_B18503.jpg]

Figure 1.9 – Umi UI Task tab

接下来,让我们将 Ant Design 组件添加到我们的项目中。

添加 Ant Design 组件

Ant Design是由蚂蚁金服用户体验设计团队创建的设计系统,以满足企业级应用开发的高要求以及这些应用中的快速变化。他们还创建了一个用于构建界面的 React UI 组件库。

资产标签页中,我们可以将 Ant Design 组件作为块添加到我们的页面中:

![Figure 1.10 – Umi UI 预览演示按钮]

![img/Figure_1.10_B18503.jpg]

Figure 1.10 – Umi UI 预览演示按钮

小贴士

目前 Umi UI 的资产标签页几乎全部是中文。尽管如此,您仍然可以通过点击预览演示并更改网站语言为英文来参考 Ant Design 文档。

让我们添加一个登录表单来实验这个功能:

  1. 导航到http://localhost:8000并打开 Umi UI 的资产标签页。

  2. 表单登录框组件中点击添加

![Figure 1.11 – 表单登录框组件添加按钮]

![img/Figure_1.11_B18503.jpg]

Figure 1.11 – 表单登录框组件添加按钮

  1. 通过点击+ 添加到此处选择第二个区域。

![Figure 1.12 – 选择添加组件的位置]

![img/Figure_1.12_B18503.jpg]

Figure 1.12 – 选择添加组件的位置

  1. 现在,在LoginForm中,确保选中的包管理器客户端是yarn,然后点击确定

![Figure 1.13 – Add Block options]

![img/Figure_1.13_B18503.jpg]

Figure 1.13 – Add Block options

等待块添加完成。Umi UI 将重新加载页面,组件已经在那里了!

如果您想,您可以为登录页面添加一些样式,如下所示:

  1. 将此代码添加到 index.less 文件中:

    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    
  2. container CSS 类添加到登录组件中:

    import React from 'react';
    import styles from './index.less';
    import LoginForm from './LoginForm';
    export default function Page() {
      return (
        <div className={styles.container}>
          <h1 className={styles.title}>
            Welcome! Access your account.</h1>
          <LoginForm />
        </div>
      );
    }
    

结果应该看起来像这样:

![图 1.14 – 带有登录表单块的登录页面图 1.14 – 带有登录表单块的登录页面

图 1.14 – 带有登录表单块的登录页面

就这样!现在你知道了如何使用 Umi UI 与你的项目进行交互。如果你喜欢这个选项,我建议你通过添加更多组件并对其进行样式化来尝试它,以便熟悉它。

摘要

在本章中,你学习了如何配置 VSCode 以与 UmiJS 一起工作。你学习了如何设置项目和整理 UmiJS 文件夹结构。你还学习了如何使用 Umi CLI 自动化任务并快速将页面和模板添加到你的项目中。

你了解到 UmiJS 项目是一个单页应用程序,以及如何在项目中定义各种配置来定义路由。你学习了如何访问路径参数和查询字符串参数。你还学习了 UmiJS 如何根据文件夹约定自动配置路由。你学习了使用 umi history 和链接组件进行导航。

最后,你学习了如何安装和使用 Umi UI 与你的项目进行交互。然后你学习了如何使用 Umi UI 执行任务,并在你的项目中添加 Ant Design 组件作为块。

在下一章中,你将学习更多关于 Umi 项目中的 Ant Design 以及如何使用它来开发界面。

第二章:使用 Ant Design 创建用户界面

遵循 antd 库的原则,该库提供了一系列 React 组件,您可以使用它们来加速用户界面开发。

在本章中,我们将研究 antd 库并使用它创建用户界面。第一部分将向您介绍我们将要开发的项目,一个 客户关系管理(CRM)应用程序。然后,我们将配置布局插件和主题。我们将创建主页并配置国际化支持(也称为 i18n)。最后,我们将创建 机会 页面、客户 页面和 报告 页面。

在本章中,我们将涵盖以下主要内容:

  • 项目和 Ant Design 介绍

  • 设置布局和主题

  • 创建主页和设置国际化

  • 创建机会和客户页面

  • 创建 报告 页面

到本章结束时,您将学会如何在 antd 库中搜索和找到满足您需求的正确组件。您将学会如何配置 plugin-layout,自定义应用程序的默认主题,并定义全局样式。您还将了解如何使用 plugin-locale 设置国际化支持。

技术要求

要完成本章的练习,您只需要一台安装了任何操作系统(我推荐 Ubuntu 20.04 或更高版本)的电脑以及 第一章* 环境设置和 UmiJS 介绍*(VScode、Node.js 和 Yarn)的软件。

您可以在 GitHub 仓库的 Chapter02 文件夹中找到完整的项目,该仓库可在以下链接找到:

github.com/PacktPublishing/Enterprise-React-Development-with-UmiJs

项目和 Ant Design 介绍

本节将向您介绍我们将要开发的项目以及 Ant Design React 库。

为了说明 UmiJS 和 Ant Design 的实际应用,我们将为 CRM 系统开发一个前端应用程序。

CRM 系统是一种商业应用程序,它允许公司接触客户,提供解决方案,并与各种战略联系人建立关系,以向客户销售正确的解决方案并保证他们的满意度。

在我们的示例中,该应用程序有三个主要功能:带有各种报告的仪表板、跟踪机会的注册和客户注册。

我们还将保证我们的应用程序在面对业务需求时易于扩展和修改,拥有清晰的代码风格,并支持国际化。

为了构建我们前端应用程序的接口,我们将使用 Ant Design React 库。让我们更深入地了解 antd 库和 Pro 组件。

Ant Design 组件介绍

Ant Design 库是一个遵循 Ant Design 系统设计原则的 React 组件库。Ant Design 库是用 TypeScript 编写的,提供可预测的静态类型、国际化支持和主题定制。该库还与 UmiJS 深度集成,因此使用 UmiJS 可以轻松定制主题和设置国际化支持。

您可以浏览库并查找组件,请访问ant.design/components/overview/。在此文档页面上,您将找到每个库组件的详细描述以及相应的代码示例。

我们还将使用来自Pro components的一些组件,这是一个由 Ant Design 派生出的组件集,以提供高级别的抽象,使构建复杂界面的任务更加容易。您可以在procomponents.ant.design/en-US/components查找 Pro components。

在本节中,您了解了 Ant Design React 库,并介绍了我们将要构建的项目。让我们通过设置默认布局和主题来开始构建界面。

设置布局和主题

在本节中,我们将使用plugin-layout设置默认布局,并自定义我们的应用程序主题,更改antd使用的默认 LESS 变量。为此,请按照以下步骤操作:

  1. 我们将使用上一章中创建的项目。将plugin-layout配置添加到config.ts文件中,如下所示:

    layout: {
        navTheme: 'light',
        layout: 'mix',
        contentWidth: 'fluid',
        fixedHeader: false,
        fixSiderbar: true,
        colorWeak: false,
        title: 'Umi CRM',
        locale: true,
        pwa: false,
        logo: 'https://img.icons8.com/ios-filled/50/ffffff/
               customer-insight.png',
        iconfontUrl: '',
    },
    

此配置为所有页面添加了页头和菜单,定义了应用程序名称和标志,并在布局组件中启用了plugin-locale

您也可以根据需要更改布局。例如,您可以将菜单设置为在页眉中显示而不是侧菜单,将layout属性更改为top

  1. 让我们还将更改所有接口和组件使用的主题主色。将此配置添加到config.ts文件中:

    theme: {
        'primary-color': '#1895bb',
    },
    

主题配置更改了 Ant Design 组件使用的默认 LESS 变量值。

重要信息

您可以在github.com/ant-design/ant-design/blob/master/components/style/themes/default.less找到所有默认的 LESS 变量。

  1. 接下来,我们需要在app.tsx文件中添加一些plugin-layout的配置。将以下内容添加到app.tsx文件中:

    import routes from '../config/routes';
    import { RunTimeLayoutConfig } from 'umi';
    export const layout: RunTimeLayoutConfig = () => {
      return {
        routes,
        rightContentRender: () => <></>,
        onPageChange: () => {},
      };
    };
    

使用此配置,我们设置了将在侧菜单上渲染的plugin-layout 路由。

  1. 为了正确显示侧菜单中的菜单项,调整路由配置如下:

routes.ts

export default [
  {
    path: '/login',
    component: '@/pages/Login',
    layout: false,
  },
  {
    path: '/',
    name: 'home',
    icon: 'home',
    component: '@/pages/Home',
  },
];

我们定义了登录页面和主页的路由。layout: false属性将使默认布局不在登录页面上显示。nameicon属性定义了Home页面在侧菜单中的显示方式。

Ant Design 提供了图标,你可以在ant.design/components/icon/查找其他图标。

现在让我们通过添加快速菜单、语言选择器并将其样式更改为使用我们的主色调来完成默认布局。

向布局头部添加右侧内容

首先,让我们创建两个新的组件:HeaderMenu,它将包含用户的头像、用户名和登出菜单项;以及HeaderOptions组件,它将包括HeaderMenuSelectLang组件。SelectLang是 UmiJS 提供的一个组件,可以通过plugin-locale在应用程序支持的语言之间切换。

按照以下步骤创建HeaderMenu组件:

  1. src文件夹中创建一个名为components的新文件夹,并在其中创建一个名为HeaderMenu的新文件夹。

  2. HeaderMenu文件夹中,创建两个文件:index.tsxindex.less

  3. index.tsx文件中,创建组件如下:

    import { Avatar, Dropdown, Menu } from 'antd';
    import styles from './index.less';
    import { LogoutOutlined } from '@ant-design/icons';
    export default function HeaderMenu() {
      const options = (
        <Menu className={styles.menu}>
          <Menu.Item key="center">
            <LogoutOutlined /> Logout
          </Menu.Item>
        </Menu>
      );
      return (
        <Dropdown 
          className={styles.dropdown} 
          overlay={options}>
          <span>
            <Avatar 
              size="small" 
              className={styles.avatar} />
            <span 
              className={`${styles.name} anticon`}>
                John Doe
            </span>
          </span>
        </Dropdown>
      );
    }
    

在此组件中,我们使用antd库的Menu组件来渲染登出菜单项,以及DropdownAvatar组件来渲染用户的头像和用户名。当鼠标悬停在用户名或头像上时,将显示登出选项。

  1. index.less文件中创建AvatarDropdown组件的 CSS 类,如下所示:

    .avatar {
      color: white;
      background-color: #1895bb;
      margin: 0px 10px;
    }
    .dropdown {
      display: flex;
      flex-flow: row nowrap;
      cursor: pointer;
      align-items: center;
      float: right;
      height: 48px;
      margin-left: auto;
      overflow: hidden;
    }
    

现在按照以下步骤创建HeaderOptions组件:

  1. components文件夹中,创建一个名为HeaderOptions的新文件夹。在其内部,创建一个名为index.tsx的文件。

  2. index.tsx文件中,创建组件如下:

    import { Space } from 'antd';
    import { SelectLang } from 'umi';
    import HeaderMenu from '../HeaderMenu';
    export default function HeaderOptions() {
      return (
        <Space>
          <HeaderMenu />
          <SelectLang />
        </Space>
      );
    }
    

在此组件中,我们使用antdSpace组件和最近创建的HeaderMenu组件以及来自 UmiJS 的SelectLang组件来渲染布局头部选项。

![Figure 2.1 – The language selector (SelectLang component)]

![Figure 2.01_B18503.jpg]

Figure 2.1 – The language selector (SelectLang component)

现在,要将HeaderOptions组件添加到布局中,请按照以下步骤操作:

  1. 导入HeaderOptions组件,将以下行添加到app.tsx文件中:

    import HeaderOptions from './components/HeaderOptions';
    
  2. HeaderOptions组件添加到rightContentRender 配置中,如下所示:

    export const layout: RunTimeLayoutConfig = () => {
      return {
        routes,
        rightContentRender: () => <HeaderOptions />,
        onPageChange: () => {},
      };
    };
    

现在,HeaderOptions组件应该如下显示在布局头部:

![Figure 2.2 – Layout right content]

![Figure 2.02_B18503.jpg]

Figure 2.2 – Layout right content

你可能已经注意到语言选择器没有出现。一旦我们向项目中添加语言支持,它就会出现。

为了完成布局的设置,让我们添加主色调。我们可以通过使用global.less文件来自定义应用于布局头部的 CSS 类来添加主色调。

UmiJS 将在所有其他样式表之前应用global.less文件,因此当你需要自定义某些样式或跨所有界面应用它时,你可以使用此文件。

按照以下步骤自定义应用于布局头部的 CSS 类:

  1. src文件夹下创建一个名为global.less的新文件。

  2. 将此样式添加到global.less文件中:

    .ant-pro-global-header-layout-mix {
      background: #1895bb;
      background: linear-gradient(50deg, #1895bb 0%, 
        #14cfbd 100%);
    }
    

我们使用主色调添加了背景渐变到 CSS 类中,并将其应用于全局页眉。

提示

您可以通过使用浏览器开发者工具检查页面来找到应用于 HTML 元素的 CSS 类。通常,您需要按F12并查找元素选项卡。

现在布局页眉应该看起来像这样:

![Figure 2.3 – 应用了主色调的布局页眉img/Figure_2.03_B18503.jpg

![Figure 2.3 – 应用了主色调的布局页眉

在本节中,我们通过配置plugin-layout和自定义global.less文件来设置所有页面的默认布局。我们还创建了渲染用户头像、用户名和语言选择器的组件。现在让我们构建主页并设置国际化。

创建主页和设置 i18n

在本节中,我们将创建主页并设置应用程序的葡萄牙语和英语国际化。

我们的主页将由两个主要组件组成:PageContainerProTable。当用户登录应用程序时,我们希望他们看到一些信息,例如用户名、角色和最近打开的机会列表。为此,请按照以下步骤操作:

  1. 让我们从向src/pages/Home文件夹下的index.tsx文件添加PageContainer组件开始,如下所示:

    import styles from './index.less';
    import { PageContainer } from '@ant-design/pro-layout';
    import { UserOutlined } from '@ant-design/icons';
    export default function IndexPage() {
      return (
        <PageContainer
          header={{ title: undefined }}
          style={{ minHeight: '90vh' }}
          content={<></>}
        ></PageContainer>
      );
    }
    

默认情况下,PageContainer组件将渲染您在routes.ts文件中定义的路由名称作为页面标题,但我们将它设置为undefined,因为我们不想在这个页面上显示标题。

  1. 现在我们将在PageContainer的内容中添加一些基本信息。我们希望当用户登录应用程序时,他们能看到问候语,然后是他们的名字、角色和头像,所以请将以下信息添加到PageContainercontent属性中,如下所示:

    content={
    <div className={styles.pageHeaderContent}>
              <div className={styles.avatar}>
                <Avatar
                  alt="avatar"
                  className={styles.avatarComponent}
                  size={{ xs: 64, sm: 64, md: 64, lg: 64, 
                    xl: 80, xxl: 100 }}
                  icon={<UserOutlined />}
                />
             </div>
             <div className={styles.content}>
               <div className={styles.contentTitle}>
                 Hello John Doe, welcome.</div>
               <div>Inside Sales | Umi Group</div>
             </div>
    </div>
    }
    

在这里,我们添加了来自antdAvatar组件,然后是问候语、用户名和角色。

  1. 我们还需要在文件index.less中定义样式。将以下样式添加到src/pages/Home文件夹下的index.less文件中,如下所示:

    @import '~antd/es/style/themes/default.less';
    .pageHeaderContent {
      display: flex;
      .avatar {
        flex: 0 1 72px;
        & > span {
          display: block;
          width: 72px;
          height: 72px;
          border-radius: 72px;
        }
        .avatarComponent {
          color: white;
          background-color: @primary-color;
        }
      }
      .content {
        position: relative;
        top: 4px;
        flex: 1 1 auto;
        margin-left: 24px;
        color: @text-color-secondary;
        line-height: 22px;
        .contentTitle {
          margin-bottom: 12px;
          color: @heading-color;
          font-weight: 500;
          font-size: 20px;
          line-height: 28px;
        }
      }
    }
    

注意,我们已从antd导入了一个名为default.less的文件。此文件包含 Ant Design 组件用于定义样式的默认 LESS 变量。我们也在我们的 CSS 类中使用了一些这些变量。

我强烈建议您熟悉这些变量;这将帮助您与 Ant Design 规范保持一致的风格。您可以通过按Ctrl并单击其导入路径来访问default.less文件,或者您可以在 GitHub 上查看该文件:github.com/ant-design/ant-design/blob/master/components/style/themes/default.less

我们将在我们的页面上添加的下一个组件是ProTable;这是一个抽象了在表格中操作一批数据逻辑的 Pro 组件组件。

  1. 要添加组件,我们需要安装其包,因此运行以下命令来完成:

    $ yarn add @ant-design/pro-table
    
  2. 接下来,在 src/pages/Home 文件夹下的 index.tsx 文件中,在 PageContainer 组件内部添加 ProTable 组件,如下所示:

    <div style={{ width: '100%' }}>
      <ProTable<any>
        headerTitle="Recent opportunities"
        pagination={{ pageSize: 5 }}
        rowKey="id"
        search={false}
      />
    </div>
    

到目前为止,您的首页应该看起来像这样:

图 2.4 – 主页界面

图 2.4 – 主页界面

现在是时候为我们的应用程序添加对 国际化i18n)的支持了。

设置国际化

要使用 plugin-locale 添加对 i18n 的支持,首先,我们必须将所有要翻译的文本移动到 src/locales 文件夹下的多语言文件中。我将使用英语和葡萄牙语构建整个应用程序来演示此功能,但您不必担心这一点;您可以下载在 github.com/PacktPublishing/Enterprise-React-Development-with-UmiJs 上可用的葡萄牙语文件。按照以下步骤创建我们的语言文件:

  1. src/locales 文件夹下创建一个名为 en-US.ts 的文件,下载 pt-BR.ts 文件,并将其放置在相同的文件夹中。

  2. en-US.ts 文件中按照以下方式输入主页的文本:

    export default {
      'home.recents': 'Recent opportunities',
      'greetings.hello': 'Hello',
      'greetings.welcome': 'welcome',
    };
    
  3. 我们需要使用 FormattedMessage 组件更改主页上的文本。在 src/pages 文件夹下的 index.ts 文件中导入组件,添加以下行:

    import { FormattedMessage } from 'umi';
    
  4. 并且按照以下方式更改组件的文本:

    <div className={styles.content}>
         <div className={styles.contentTitle}>
               <FormattedMessage id="greetings.hello" /> 
                 John Doe,{' '}
               <FormattedMessage id="greetings.welcome" />.
          </div>
          <div>Inside Sales | Umi Group</div>
    </div>
    
  5. 还要按照以下方式更改 ProTableheaderTitle 属性:

    headerTitle={<FormattedMessage id="home.recents" />}
    

FormattedMessage 组件的属性 id 必须与 en-US.tspt-BR.ts 文件中的相同键匹配。当你选择语言时,组件将渲染相应的文本。

我们需要翻译菜单标题,所以让我们添加要翻译的菜单项文件。按照以下步骤操作:

  1. src/locales 文件夹下创建一个名为 en-US 的新文件夹。在 en-US 文件夹下,创建一个名为 menu.ts 的新文件。

  2. 将要渲染在菜单项中的文本添加到 menu.ts 文件中,如下所示:

    export default {
      'menu.home': 'Home',
    };
    

文本的键需要与 routes.ts 文件中的 name 属性匹配。plugin-locale 将在您在语言之间切换时渲染正确的文本。

  1. 按照以下方式将 menu.ts 文件导入到 en-US.ts 文件中:

    import menu from './en-US/menu';
    export default {
      ...menu,
      'home.recents': 'Recent opportunities',
      'greetings.hello': 'Hello',
      'greetings.welcome': 'welcome',
    };
    
  2. 我们还需要将 menu.ts 文件添加到葡萄牙语中,所以在 src/locales 文件夹下创建一个名为 pt-BR 的新文件夹,从 github.com/PacktPublishing/Enterprise-React-Development-with-UmiJs 下载可用的 menu.ts 文件,并将其放置在 pt-BR 文件夹下。

现在您可以使用页面顶部的语言选择器更改应用程序的语言,如下面的截图所示:

图 2.5 – 选择葡萄牙语的主页

图 2.5 – 选择葡萄牙语的主页

在本节中,我们使用 PageContainerProTable 组件创建了主页。我们还通过在 src/locales 文件夹下创建多语言文件并使用 FormattedMessage 组件替换文本为相应的翻译来设置国际化。

现在,您将使用所学知识来创建 机会客户 页面。

创建机会和客户页面

在本节中,我们将构建 机会客户 页面。

机会 页面允许用户浏览和注册新的销售机会。当客户似乎对购买产品或服务感兴趣时,就会发生销售机会。在 机会 页面上,我们跟踪所有活动,直到机会被赢得(客户购买产品或服务),或者直到机会丢失(客户购买竞争对手的产品或取消购买)。

客户 页面允许用户注册和搜索客户的联系信息。

这两个页面相似;它们使用 ProTable 组件列出注册的机会和客户。运行以下命令生成两个页面:

$ yarn umi g page /Customers/index --typescript --less
$ yarn umi g page /Opportunities/index --typescript --less

现在,让我们从 客户 页面开始。按照以下步骤构建 客户 页面界面:

  1. PageContainerProTable 组件添加到 src/pages/Customer 文件夹下的 index.tsx 文件中,如下所示:

    import { PlusOutlined } from '@ant-design/icons';
    import { Button } from 'antd';
    import ProTable from '@ant-design/pro-table';
    import { FormattedMessage, getLocale } from 'umi';
    import { PageContainer } from '@ant-design/pro-layout';
    export default function Page() {
      return (
        <PageContainer style={{ minHeight: '90vh' }}>
          <ProTable<any>
            rowKey="id"
            headerTitle=
              {<FormattedMessage id="table.customer.title" 
              />}
            search={{ labelWidth: 'auto' }}
            pagination={{ pageSize: 5 }}
            dateFormatter="string"
            locale={getLocale()}
            toolBarRender={() => [
              <Button key="button" icon={<PlusOutlined />} 
                type="primary">
                <FormattedMessage id="table.new" />
              </Button>,
            ]}
          />
        </PageContainer>
      );
    }
    

注意,我们使用 FormattedMessage 组件在此页面上渲染文本,因此我们需要将这些文本添加到 src/locales 文件夹中的多语言文件中。

  1. 按照以下方式将 en-US.ts 文件中的文本添加:

    import menu from './en-US/menu';
    export default {
      ...menu,
      'home.recents': 'Recent opportunities',
      'greetings.hello': 'Hello',
      'greetings.welcome': 'welcome',
      'table.opportunity.title': 'Opportunities',
      'table.customer.title': 'Customers',
    };
    
  2. 现在,要访问 routes.ts 文件,请按照以下方式:

    {
        path: '/customers',
        name: 'customers',
        icon: 'user',
        component: '@/pages/Customers',
    },
    
  3. 将客户菜单项标题添加到 src/locales/en-US 文件夹下的 menu.ts 文件中,如下所示:

    export default {
      'menu.opportunities': 'Opportunities',
      'menu.customers': 'Customers',
    };
    

现在,按照之前演示的步骤构建 机会 页面:

  1. 对于 ProTableheaderTitle 属性,请输入以下内容:

    headerTitle={<FormattedMessage id="table.opportunity.title" />}
    
  2. 定义 机会 页面的路由如下:

    {
        path: '/opportunities',
        name: 'opportunities',
        icon: 'AccountBook',
        component: '@/pages/Opportunities',
    },
    
  3. 不要忘记将文本添加到 en-US.tsmenu.ts 文件中。

结果应该看起来像这样:

![图 2.6 – 机会页面布局和菜单项

![img/Figure_2.06_B18503.jpg]

图 2.6 – 机会页面布局和菜单项

在本节中,我们创建了支持国际化的 ProTable 组件。接下来,我们将构建 报告 页面。

创建报告页面

现在,我们将构建 报告 页面。用户可以在此页面上获取有用的信息,以深入了解销售生命周期。我们将使用 bizcharts 组件库在此页面上添加三个图表。

bizcharts 库专注于商业场景,致力于创建专业的数据可视化解决方案。它也是一个开源项目,遵循 MIT 许可协议。您可以在 bizcharts.taobao.com/ 了解更多关于 bizcharts 的信息:

  1. 首先,运行以下命令来安装 bizcharts 包:

    $ yarn add bizcharts
    
  2. 接下来,运行以下命令以生成报告页面:

    $ yarn umi g page /Reports/index --typescript --less
    

现在,按照以下步骤创建报告页面界面:

  1. 让我们使用antd组件定义页面布局。将以下组件添加到src/pages/Reports文件夹下的index.tsx文件:

    import { PageContainer } from '@ant-design/pro-layout';
    import { Row, Col, Card } from 'antd';
    import { FormattedMessage } from 'umi';
    import {
      Chart,
      Coordinate,
      Axis,
      Legend,
      Interval,
      Tooltip,
      Interaction,
    } from 'bizcharts';
    const colProps = {
      style: { marginBottom: 24 },
      xs: 24,
      sm: 12,
      md: 12,
      lg: 12,
      xl: 12,
    };
    export default function Page() {
      return (
        <PageContainer>
          <Row gutter={24}>
            <Col {...colProps}></Col>
            <Col {...colProps}></Col>
          </Row>
          <Row gutter={24} style={{ padding: 10 }}></Row>
        </PageContainer>
      );
    }
    

我们使用两个响应式行定义了布局,第一行有两个响应式列。colProps变量设置列在不同断点处如何调整其大小。

  1. 现在,让我们添加第一个图表。这个图表将显示由 CRM 分析服务分类的前四个最重要的机会。如下将 bizcharts 中的Chart组件添加到第一行的第一列:

    <Card title={<FormattedMessage id="chart.top" />}>
      <Chart height={200} data={[]} autoFit>
        <Coordinate transpose />
        <Axis name="name" label={false} />
        <Axis
          name="revenue"
          label={{
            formatter: (text) => `$ ${text}`,
          }}
        />
        <Interval
          type="interval"
          label={["name", (name) => <>{name}</>]}
          tooltip={{
            fields: ["name", "revenue"],
            callback: (name, revenue) => {
              return { name: name, value: `$ ${revenue}` };
            },
          }}
          color={["name", "#3936FE-#14CCBE"]}
          position="name*revenue"
        />
          <Interaction type="element-single-selected" />
      </Chart>
    </Card>
    

我们可以使用其子组件配置Chart组件。我们使用Coordinate组件将图表设置为反转xy轴。使用Axis组件,我们定义了一个名为revenue的新轴。Interval组件描述了图表类型和将使用position属性填充轴的键。

注意,我们在data属性中设置了一个空数组。我们将在未来将想要显示的信息放入data属性中。

  1. 让我们添加第二个图表。这个图表将显示客户来源及其比例。如下将Chart组件添加到第一行的第二列:

    <Card title={<FormattedMessage id="chart.leads" />}>
      <Chart
        height={200}
        data={[]}
        scale={{
          percent: {
            formatter: (val: any) => {
              val = val * 100 + "%";
              return val;
            },
          },
        }}
        autoFit
      >
        <Coordinate type="theta" radius={0.95} />
        <Tooltip showTitle={false} />
        <Axis visible={false} />
        <Legend position="right" />
        <Interval
          position="percent"
          adjust="stack"
          color="source"
          style={{
            lineWidth: 1,
            stroke: "#fff",
          }}
        />
        <Interaction type="element-single-selected" />
      </Chart>
    </Card>
    

在此图表中,我们将Coordinate组件设置为圆柱坐标以生成饼图。使用Interaction组件,我们将图表设置为在鼠标悬停或点击时做出反应。

  1. 最后一个图表显示了按月获得和失去的机会。如下将Chart组件添加到第二行:

    <Card
      style={{ width: '100%' }}
      title={<FormattedMessage id="chart.month" />}
    >
      <Chart height={300} padding="auto" data={[]} autoFit>
        <Interval
          adjust={[
            {
              type: 'dodge',
              marginRatio: 0,
            },
          ]}
          color={['name', '#3776E7-#14CCBE']}
          position="month*value"
        />
        <Tooltip shared />
      </Chart>
    </Card>
    
  2. 要完成src/locales文件夹下的en-US.ts文件:

    'chart.top': 'Top opportunities',
    'chart.leads': 'Leads by source',
    'chart.month': 'Opportunities Won/Lost by month',
    
  3. 并将以下文本添加到src/locales/en-US文件夹下的menu.ts文件:

    'menu.reports': 'Reports',
    
  4. 最后,按照以下方式配置routes.ts文件:

    {
      path: '/reports',
      name: 'reports',
      icon: 'BarChartOutlined',
      component: '@/pages/Reports',
    },
    

现在,报告页面已完成,应该看起来像这样:

图 2.7 – 报告页面布局

图 2.7 – 报告页面布局

注意,所有图表卡片都是空的,因为我们定义了所有图表data属性中的空数组。我们将在下一章生成显示图表所需的数据。

在本节中,我们使用 bizcharts 库创建了报告页面。我们在页面上添加了三个图表:一个名为顶级机会的柱状图,一个名为来源线索的饼图,以及一个名为按月赢得/失去的机会的柱状图。

摘要

在本章中,你被介绍到我们将要构建的项目,Ant Design React 库和 Pro 组件。你还学习了如何使用 UmiJS 布局插件配置布局,并使用global.less文件定义和自定义全局布局。你学习了如何通过更改 Ant Design 组件使用的默认 LESS 变量来自定义应用程序主题。

我们还创建并定义了应用布局右侧内容,以显示用户的姓名、头像和语言选择器。你学习了如何使用 UmiJS 本地化插件来设置国际化,并创建了主页。接下来,我们制作了 ProTable 组件。

最后,我们使用 antd 库组件来定义布局,并使用 bizcharts 库来渲染三个图表来构建了 报告 页面。

在下一章中,我们将生成模拟 API,向后端发送请求,并学习如何使用服务和模型。

第三章:使用模型、服务和模拟数据

前端 Web 应用程序的主要功能之一是与后端通信。我们的应用程序需要收集用户输入并将其发送进行处理。

在本章中,你将学习如何通过创建 TypeScript 接口和 ProTable 组件的列定义来定义数据。你将学习如何使用 Umi mock 文件来模拟后端逻辑和数据。你将了解如何使用 umi-request 库发送 HTTP 请求。你还将学习如何使用 models 在组件之间共享状态和逻辑。

我们将涵盖以下主要主题:

  • 定义响应类型和列类型

  • 创建机会详情页面

  • 模拟数据和 API 响应

  • 使用 Umi 请求发送 HTTP 请求

  • 使用模型共享状态和逻辑

到本章结束时,你将学会如何在 Umi 中处理数据流,以及如何使用 servicesmodels 文件夹来组织你的项目。你还将了解如何使用 Umi 功能来模拟后端逻辑和发送 HTTP 请求。你还将更好地理解 ProTable 组件如何帮助我们处理数据批量操作。

技术要求

要完成本章的练习,你只需要一台安装了任何操作系统(我推荐 Ubuntu 20.04 或更高版本)的计算机,以及安装在 第一章 环境设置和 UmiJS 简介(Visual Studio Code、Node.js 和 Yarn)的软件。

你可以在 GitHub 仓库的 Chapter03 文件夹中找到完整的项目,该仓库的网址为 github.com/PacktPublishing/Enterprise-React-Development-with-UmiJs

定义响应类型和列类型

在本节中,我们将创建 TypeScript 接口来定义我们将从后端接收的数据,并为每个页面的 ProTable 组件创建列定义。

让我们从接口开始。按照以下步骤创建 TypeScript 接口:

  1. 让我们首先创建定义文件的文件夹。在 src 文件夹下,创建一个名为 types 的新文件夹。

  2. 现在,在 types 文件夹中,创建一个名为 user.d.ts 的新文件,并添加以下接口代码:

    export interface User {
      id?: number;
      name?: string;
      company?: string;
      role?: {
        id: number;
        title: string;
      };
      isLoggedIn: boolean;
    }
    

User 接口定义了我们将如何从后端接收用户信息。

  1. types 文件夹中创建一个名为 customer.d.ts 的新文件,并添加以下接口:

    export interface Customer {
      id?: number;
      name?: string;
      company?: string;
      phone?: string;
      email?: string;
      role?: string;
    }
    

Customer 接口定义了我们将如何从后端接收客户信息。

  1. 现在,我们将为 opportunity 模型创建两个接口。在 types 文件夹中创建一个名为 opportunity.d.ts 的新文件,并添加以下接口:

    import { Customer } from './customer';
    export interface Opportunity {
      id: number;
      topic: string;
      budget: string;
      status: number;
      customer: Customer;
    }
    export interface Activity {
      id: number;
      type: number;
      schedule: Date;
      createdBy: string;
      summary: string;
    }
    

Opportunity 接口定义了我们将如何从后端接收机会信息。

注意,我们导入了 Customer 接口并将其用作 customer 属性的类型。一个机会总是与 CRM 中注册的特定客户相关联。

Activity接口定义了我们从后端接收机会活动信息的方式。

  1. 让我们创建报告数据的接口。在types文件夹中创建一个名为analytics.d.ts的新文件,并添加以下接口:

    export interface TopOpportunity {
      name: string;
      revenue: string;
    }
    export interface LeadsSource {
      source: string;
      count: number;
      percent: number;
    }
    export interface HistoryByMonth {
      name: string;
      month: string;
      value: string;
    }
    

这些接口定义了我们将如何接收顶级机会图表、来源图表以及按月赢得/失去的机会图表的数据。

现在,让我们定义每个页面的ProTable组件应该如何显示从后端接收到的数据。

创建 ProTable 的列定义

我们需要通过定义列来设置ProTable组件将如何显示数据。我建议您尽可能在单独的文件中创建列定义,以保持组件代码的整洁。

按照以下步骤在每个页面上创建列定义:

  1. 让我们从src/pages文件夹下的Customers文件夹中的columns.tsx文件开始。

  2. columns.tsx文件中,按照如下方式定义表格列:

    import { Customer } from '@/types/customer';
    import { ProColumns } from '@ant-design/pro-table';
    import { FormattedMessage } from 'umi';
    const columns: ProColumns<Customer>[] = [
      {
        title: <FormattedMessage id="table.customer.name" 
               />,
        dataIndex: 'name',
      },
      {
        title: <FormattedMessage id="table.customer.email"
               />,
        dataIndex: 'email',
        copyable: true,
      },
      {
        title: <FormattedMessage id="table.customer.phone"
               />,
        dataIndex: 'phone',
      },
      {
        title: <FormattedMessage id="table.customer.role" 
               />,
        dataIndex: 'role',
      },
      {
        title: <FormattedMessage  
                id="table.customer.company" />,
        dataIndex: 'company',
      },
    ];
    export default columns;
    

注意,我们使用了Customer接口来声明数据类型。每个列定义都有一个title和一个dataIndex。后者需要与Customer接口的一个属性匹配,以便ProTable可以在其列中显示该属性值。

  1. 让我们添加一个列来显示特定行的选项。将此定义添加到columns.tsx文件中:

    {
      title: <FormattedMessage id="table.options" />,
      valueType: 'option',
      hideInSetting: true,
      hideInDescriptions: true,
      render: (_, record, __, action) => [
        <a
          key="editable"
          onClick={() => {
            action?.startEditable(record.id as number);
          }}
        >
          <FormattedMessage id="table.edit" />
        </a>,
      ],
    },
    

options列中,除了我们设置为不在设置和描述中显示的属性外,我们还设置了render函数的行为。这允许您访问 React 节点、行实体、索引和默认的ProTable操作。当用户点击此选项时,startEditable操作允许他们编辑行。

  1. 现在,我们将使用ProTable组件中的列定义。将以下行添加到index.tsx文件中,以导入columns.tsx文件:

    import columns from './columns';
    
  2. 使用导入的文件定义ProTablecolumns属性,如下所示:

    columns={columns}
    
  3. 接下来,将Customer接口添加到定义ProTable列的数据类型:

    <ProTable<Customer>
    
  4. 现在,让我们创建src/pages文件夹下Opportunities文件夹中的columns.tsx的列定义。

  5. 按照如下方式将定义添加到columns.tsx文件中:

    import { Customer } from '@/types/customer';
    import { Opportunity } from '@/types/opportunity';
    import { ProColumns } from '@ant-design/pro-table';
    import { Tag } from 'antd';
    import { FormattedMessage, history } from 'umi';
    const columns: ProColumns<Opportunity>[] = [
      {
        title: <FormattedMessage
                id="table.opportunity.topic" />,
        dataIndex: 'topic',
        width: 300,
      },
      {
        title: <FormattedMessage
                id="table.opportunity.budget" />,
        dataIndex: 'budget',
        render: (node) => <>{`$ ${node}`}</>,
      },
      {
        title: <FormattedMessage
                id="table.opportunity.status" />,
        dataIndex: 'status',
        valueType: 'select',
        hideInDescriptions: true,
        filters: true,
        onFilter: true,
      },
    ];
    export default columns;
    

注意,我们使用了之前创建的Opportunity接口来定义数据类型。

status列中,我们将valueType属性设置为select,并将filtersonFilter属性设置为true,这样用户就可以使用此column值选择和过滤表格。

  1. status是一个表示销售流程中机会进度的数值。但是,我们希望用户看到的是标题而不是数字,所以让我们给status列添加enumType属性,如下所示:

    valueEnum: {
      0: {
        text: (
          <Tag color="#8d79f2" key={0}>
            <FormattedMessage id="step.propose" />
          </Tag>
        ),
      },
      1: {
        text: (
          <Tag color="#c7f279" key={0}>
            <FormattedMessage id="step.develop" />
          </Tag>
        ),
      },
      2: {
        text: (
          <Tag color="#e379f2" key={0}>
            <FormattedMessage id="step.qualify" />
          </Tag>
        ),
      },
      3: {
        text: (
          <Tag color="#79f2e3" key={0}>
            <FormattedMessage id="step.close" />
          </Tag>
        ),
      },
    },
    

状态将以不同颜色和相应的步骤标题显示为标签。

  1. 机会与客户相关联,我们需要定义客户属性列。让我们按照如下方式添加这些列:

    {
      title: <FormattedMessage  
              id="table.opportunity.customer" />,
      dataIndex: 'customer',
      render: (node) => <>{node && (node as 
                                    Customer).name}</>,
      editable: false,
    },
    {
      title: <FormattedMessage id="table.customer.email"
             />,
      dataIndex: 'customer',
      hideInTable: true,
      render: (node) => <>{node && (node as 
                                    Customer).email}</>,
      editable: false,
    },
    {
      title: <FormattedMessage id="table.customer.phone" 
             />,
      dataIndex: 'customer',
      hideInTable: true,
      render: (node) => <>{node && (node as
                                    Customer).phone}</>,
      editable: false,
    },
    {
      title: <FormattedMessage id="table.customer.company"
             />,
      dataIndex: 'customer',
      hideInTable: true,
      render: (node) => <>{node && (node as 
                                    Customer).company}</>,
      editable: false,
    },
    

注意,表中只有一列是客户的姓名。在其他列中,我们将hideInTable设置为true。我们将在本章后面创建的机会详情页面中使用这些列。

  1. 我们还将添加一个列来显示特定行的选项。将以下定义添加到columns.tsx文件中:

    {
      title: <FormattedMessage id="table.options" />,
      valueType: 'option',
      hideInSetting: true,
      hideInDescriptions: true,
      render: (_, record, __, action) => [
        <a
          key="editable"
          onClick={() => {
            action?.startEditable(record.id as number);
          }}
        >
          <FormattedMessage id="table.edit" />
        </a>,
        <a key="more" onClick={() => 
          history.push(`/opportunity/${record.id}`)}>
          <FormattedMessage id="table.more" />
        </a>,
      ],
    },
    

options列引入了两个选项 - /opportunity/:id路径。

  1. 现在,我们将在ProTable组件中使用列定义。将以下行添加到index.tsx文件中,以导入columns.tsx文件:

    import columns from './columns';
    
  2. 使用导入的文件定义ProTablecolumns属性,如下所示:

    columns={columns}
    
  3. 导入Opportunity接口,如下所示:

    import { Opportunity } from '@/types/opportunity';
    
  4. 接下来,添加Opportunity接口以定义ProTable列的数据类型:

    <ProTable<Opportunity>
    
  5. 按照之前的步骤将列定义添加到主页上的ProTable。我们将重用机会页面上的columns.tsx文件,因此您需要导入该文件,如下所示:

    import columns from '../Opportunities/columns';
    
  6. 现在,我们需要将文本添加到多语言文件中。将文本添加到src/locales文件夹中的en-US.ts文件中,如下所示:

    'table.options': 'Options',
    'table.edit': 'Edit',
    'table.more': 'More',
    'table.new': 'New',
    'table.customer.title': 'Customers',
    'table.customer.role': 'Role',
    'table.customer.name': 'Name',
    'table.customer.email': 'Email',
    'table.customer.phone': 'Phone',
    'table.customer.company': 'Company',
    'form.customer.title': 'New customer',
    'table.opportunity.assign': 'Assign Opportunities',
    'table.opportunity.title': 'Opportunity',
    'table.opportunity.detail': 'Details',
    'table.opportunity.activities': 'Activities',
    'table.opportunity.topic': 'Topic',
    'table.opportunity.budget': 'Budget',
    'table.opportunity.status': 'Step',
    'table.opportunity.customer': 'Customer',
    'form.opportunity.title': 'New opportunity',
    

我们不需要将这些文本添加到pt-BR.ts文件中,因为我们已经在上一章中下载了完整的文件。

在本节中,我们创建了 TypeScript 接口来定义我们将从后端接收的所有数据以及每个页面上ProTable组件的列定义。

现在,让我们创建机会详情页面,该页面将显示机会上的活动。

创建机会详情页面

在本节中,我们将创建机会详情页面。机会详情页面允许用户跟踪和注册机会活动。

用户还可以更改机会步骤并编辑机会属性,如titleexpected revenue

按照以下步骤创建机会详情页面:

  1. 运行以下命令以生成机会详情页面:

    yarn umi g page /OpportunityDetail/index --typeScript --less
    
  2. 我们将使用一个名为ProDescriptions的专业组件。它与ProTable组件类似,但旨在显示属性而不是数据批量。运行以下命令将ProDescriptions组件添加到项目中:

    yarn add @ant-design/pro-descriptions@1.10.5
    
  3. 接下来,将这些依赖项导入到OpportunityDetail文件夹中的index.tsx文件中,如下所示:

    import { Opportunity } from '@/types/opportunity';
    import ProDescriptions from '@ant-design/pro-descriptions';
    import { Page Container } from '@ant-design/pro-layout';
    import ProTable from '@ant-design/pro-table';
    import { Breadcrumb, Button, Card, Steps, Tag } from 'antd';
    import { useParams, history, FormattedMessage } from 'umi';
    import columns from '../Opportunities/columns';
    import { PlusOutlined } from '@ant-design/icons';
    import { Activity } from '@/types/opportunity';
    
  4. 现在,在index.tsx文件中创建page组件,如下所示:

    export default function Page() {
      const { id } = useParams<{ id: string }>();
      return (
        <PageContainer
          extra={[
            <Button icon={<PlusOutlined />} key="activity" 
             type="primary">
              <FormattedMessage id="activity.new" />
            </Button>,
          ]}
        >
          <Card bordered>
            <ProDescriptions<Opportunity>
              title={<FormattedMessage 
                     id="table.opportunity.detail" />}
              columns={columns}
              dataSource={[]}
            />
          </Card>
          <Card bordered>
            <ProTable<Activity>
              headerTitle={<FormattedMessage 
                id="table.opportunity.activities" />}
              rowKey="id"
              toolbar={{ settings: undefined }}
              search={false}
              pagination={{ pageSize: 5 }}
              columns={[]}
              params={{ customerId: id }}
              request={() => {}}
            />
          </Card>
        </PageContainer>
      );
    }
    

注意,我们访问了从路由参数中获得的 ID。我们将使用它从后端请求特定的机会。

我们还使用了ProDescriptions组件来显示机会详情,以及ProTable来列出机会活动。

  1. 机会详情页面仅在opportunities表格行选项中可访问,不在菜单中可访问,因此让我们向PageContainer组件添加一个header属性,如下所示:

    header={{
      title: <FormattedMessage 
              id="table.opportunity.title" />,
      breadcrumb: (
        <Breadcrumb>
          <Breadcrumb.Item>
            <a onClick={() => 
              history.push('/opportunities')}>
              <FormattedMessage id="menu.opportunities" />
            </a>
          </Breadcrumb.Item>
          <Breadcrumb.Item>
            <FormattedMessage id="table.opportunity.title"
            />
          </Breadcrumb.Item>
        </Breadcrumb>
      ),
    }}
    
  2. 现在,我们将从 Ant Design 添加Steps组件来直观地显示机会的进度。将Steps组件添加到ProDescriptions组件之前,作为Card组件的第一个子组件,如下所示:

    <Steps current={0}>
      <Steps.Step
        key="quality"
        description={<Tag color="#e379f2" key={0} />}
        title={<FormattedMessage id="step.qualify" />}
      />
      <Steps.Step
        key="develop"
        description={<Tag color="#c7f279" key={1} />}
        title={<FormattedMessage id="step.develop" />}
      />
      <Steps.Step
        key="propose"
        description={<Tag color="#8d79f2" key={2} />}
        title={<FormattedMessage id="step.propose" />}
      />
      <Steps.Step
        key="close"
        description={<Tag color="#42C3E3" key={3} />}
        title={<FormattedMessage id="step.close" />}
      />
    </Steps>
    <br />
    

current属性表示机会在销售流程中的进度。

  1. 现在,将路由定义添加到config文件夹中的routes.ts文件中的机会详情页面,如下所示:

    {
      path: '/opportunity/:id',
      component: '@/pages/OpportunityDetail',
    },
    

注意,我们未添加nameicon属性,因为我们不希望在侧菜单中列出机会详情页面。

我们几乎完成了机会详情页面的构建。现在,我们将使用之前创建的Activity接口来定义活动表格的列。

定义活动表格列

机会详情页面使用ProTable组件列出所采取的活动,因此我们还需要为此表格定义列。

按照以下步骤为ProTable组件定义列:

  1. src/pages文件夹下的OpportunityDetails文件夹中创建一个名为columns.tsx的新文件。

  2. 接下来,将列定义添加到columns.tsx文件中,如下所示:

    import { ProColumns } from '@ant-design/pro-table';
    import { FormattedMessage } from 'umi';
    import { Activity } from '@/types/opportunity';
    import { Tag } from 'antd';
    const columns: ProColumns<Activity>[] = [
      {
        title: <FormattedMessage 
                id="table.activity.summary" />,
        dataIndex: 'summary',
        width: 300,
      },
      {
        title: <FormattedMessage id="table.activity.type"
               />,
        dataIndex: 'type',
      },
      {
        title: <FormattedMessage 
                id="table.activity.schedule" />,
        valueType: 'date',
        dataIndex: 'schedule',
      },
      {
        title: <FormattedMessage 
                id="table.activity.createdBy" />,
        dataIndex: 'createdBy',
      },
    ];
    export default columns;
    

type属性是一个表示活动类型的数值。

  1. 我们希望用户在type列中看到标题而不是数值,因此让我们向type列添加enumType属性,如下所示:

    valueEnum: {
      0: {
        text: (
          <Tag color="#42C3E3" key={0}>
            <FormattedMessage id="activity.call" />
          </Tag>
        ),
      },
      1: {
        text: (
          <Tag color="#42C3E3" key={1}>
            <FormattedMessage id="activity.email" />
          </Tag>
        ),
      },
      2: {
        text: (
          <Tag color="#42C3E3" key={2}>
            <FormattedMessage id="activity.meeting" />
          </Tag>
        ),
      },
      3: {
        text: (
          <Tag color="#42C3E3" key={3}>
            <FormattedMessage id="activity.event" />
          </Tag>
        ),
      },
    },
    

现在,type列将以不同颜色显示带有活动类型标题的标签。

  1. 现在,我们将使用ProTable组件中的列定义。将以下行添加到index.tsx文件中,以导入columns.tsx文件:

    import activityColumns from './columns';
    
  2. 接下来,使用导入的文件定义ProTablecolumns属性,如下所示:

    columns={activityColumns}
    
  3. 为了完成机会详情页面,让我们将文本添加到src/locales文件夹中的en-US.ts文件中,如下所示:

    'step.qualify': 'Qualify',
    'step.develop': 'Develop',
    'step.propose': 'Propose',
    'step.close': 'Close',
    'activity.call': 'Call',
    'activity.email': 'Email',
    'activity.meeting': 'Meeting',
    'activity.event': 'Event',
    'activity.new': 'New activity',
    'table.activity.summary': 'Summary',
    'table.activity.type': 'Type',
    'table.activity.schedule': 'Scheduled',
    'table.activity.createdBy': 'User',
    

在本节中,我们创建了机会详情页面。我们添加了一个面包屑来帮助用户在界面之间导航,并使用Steps组件来显示机会进度。我们还通过创建columns.tsx文件来定义活动表格列。

现在,我们已经准备好通过创建 Umi 模拟文件来学习如何模拟后端逻辑和 API 响应。

模拟数据和 API 响应

在本节中,您将学习如何创建模拟文件来模拟后端逻辑和 API 响应。

模拟文件有助于解耦前端开发和后端开发,因为您不需要后端准备好才能进行请求并使用数据填充您的界面。

模拟文件只是一个具有端点路由定义和对每个端点的响应的 JavaScript 对象。考虑以下示例:

export default {
  'GET /api/products': { total: 0, products: [] },
};

在此示例中,当项目运行时,我们可以向 http://localhost:8000/api/products 发送HTTP GET请求,以接收模拟文件中定义的对象。

Umi 会将mock文件夹内所有.js.ts扩展名的文件注册为模拟文件。

现在我们知道了模拟文件是如何工作的,让我们为我们的应用程序创建模拟文件。按照以下步骤操作:

  1. 在项目根目录中,创建一个名为 mock 的新文件夹。

  2. 我们将使用名为 faker.js 的库来生成我们想要发送的数据,而不是手动生成数据,这个库提供了我们可以选择的各种数据类别,例如用户资料、公司联系人和产品信息。运行以下命令来安装 faker.js 库:

    $ yarn add -D faker@5.5.3
    
  3. 让我们通过运行以下命令添加 faker.js 的 TypeScript 声明文件:

    $ yarn add -D @types/faker
    
  4. 接下来,通过运行以下命令添加 express 的 TypeScript 声明文件:

    $ yarn add -D @types/express
    

我们将使用 expressResponseRequest 接口来定义我们的模拟端点中的请求和响应。

  1. 现在,在 mock 文件夹中创建一个名为 customer.ts 的新文件。

  2. customer.ts 文件中创建客户列表,如下所示:

    import * as faker from 'faker';
    import { Response } from 'express';
    import { Customer } from '@/types/customer.d';
    const customers: Customer[] = [];
    for (let index = 0; index < 30; index++) {
      customers.push({
        id: index,
        name: faker.name.findName(),
        company: faker.company.companyName(),
        phone: faker.phone.phoneNumber(),
        role: faker.name.jobTitle(),
        email: faker.internet.email(),
      });
    }
    

我们使用了 faker.js 库来生成随机的客户属性。

  1. 接下来,在 customer.ts 文件中创建端点路由定义,如下所示:

    export default {
      'PUT /api/customer': (_: any, res: Response) => 
    res.send({ success: true }),
      'PUT /api/customer/disable': (_: any, res: Response) 
    =>
        res.send({ success: true }),
      '/api/customer/list': (_: any, res: Response) =>
        res.send({ data: customers, success: true }),
      'POST /api/customer': (_: any, res: Response) =>
        res.status(201).send({ success: true }),
    };
    

我们定义了四个端点:

  • 一个用于更新客户记录的模拟端点(PUT /api/customer

  • 一个用于禁用客户记录的模拟端点(PUT /api/customer/disable

  • 一个用于列出所有客户的模拟端点(/api/customer/list

  • 一个用于创建新客户记录的模拟端点(POST /api/customer

注意,当端点使用 GET 方法时,例如在列出所有客户的端点(/api/customer/list)中,我们不需要定义 HTTP 方法。

  1. 现在,在 mock 文件夹中创建一个名为 analytics.ts 的新文件,并添加以下内容:

    import * as faker from 'faker';
    import { Response } from 'express';
    export default { }
    

我们将在该文件中创建数据以填充 报告 页面上的图表。

  1. analytics.ts 文件中,创建一个新的模拟端点以交付顶级机会,如下所示:

    '/api/analytics/top/opportunity': (_: any, res: Response) =>
        res.send({
          data: [
            { name: faker.commerce.productName(), 
              revenue: 15000 },
            { name: faker.commerce.productName(), 
              revenue: 30000 },
            { name: faker.commerce.productName(), 
              revenue: 40000 },
            { name: faker.commerce.productName(), 
              revenue: 50000 },
          ],
          success: true,
        }),
    
  2. 接下来,创建一个模拟端点以按来源交付潜在客户,如下所示:

    '/api/analytics/leads/source': (_: any, res: Response) =>
        res.send({
          data: [
            { source: 'Social Media', count: 40,
               percent: 0.4 },
            { source: 'Email Marketing', count: 21, 
              percent: 0.21 },
            { source: 'Campaigns', count: 17, 
              percent: 0.17 },
            { source: 'Landing Page', count: 13, 
              percent: 0.13 },
            { source: 'Events', count: 9, percent: 0.09 },
          ],
          success: true,
        }),
    
  3. 最后,创建一个按月交付赢得/失去的机会的模拟端点,如下所示:

    '/api/analytics/bymonth/opportunity': (_: any, res: Response) =>
        res.send({
          data: [
            { name: 'Won', month: 'Jan.', value: 18 },
            { name: 'Won', month: 'Feb.', value: 28 },
            { name: 'Won', month: 'Mar.', value: 39 },
            { name: 'Won', month: 'Apr.', value: 81 },
            { name: 'Won', month: 'May', value: 47 },
            { name: 'Won', month: 'Jun.', value: 20 },
            { name: 'Won', month: 'Jul.', value: 24 },
            { name: 'Won', month: 'Aug.', value: 35 },
            { name: 'Lost', month: 'Jan.', value: 12 },
            { name: 'Lost', month: 'Feb.', value: 23 },
            { name: 'Lost', month: 'Mar.', value: 34 },
            { name: 'Lost', month: 'Apr.', value: 99 },
            { name: 'Lost', month: 'May', value: 52 },
            { name: 'Lost', month: 'Jun.', value: 35 },
            { name: 'Lost', month: 'Jul.', value: 37 },
            { name: 'Lost', month: 'Aug.', value: 42 },
          ],
          success: true,
        }),
    
  4. 现在,我们将为机会创建一个模拟文件。在 mock 文件夹中创建一个名为 opportunity.ts 的新文件,并添加以下内容:

    import * as faker from 'faker';
    import { Opportunity, Activity } from '@/types/opportunity.d';
    import { Request, Response } from 'express';
    const opportunity: Opportunity[] = [];
    const activities: Activity[] = [];
    for (let index = 0; index < 5; index++) {
      activities.push({
        id: index,
        type: faker.datatype.number({ max: 3, min: 0,
          precision: 1 }),
        schedule: faker.date.recent(),
        createdBy: faker.name.findName(),
        summary: faker.lorem.words(6),
      });
    }
    for (let index = 0; index < 30; index++) {
      opportunity.push({
        id: index,
        topic: faker.commerce.productName(),
        customer: {
          id: index,
          name: faker.name.findName(),
          company: faker.company.companyName(),
          phone: faker.phone.phoneNumber(),
          role: faker.name.jobTitle(),
          email: faker.internet.email(),
        },
        budget: faker.finance.amount(100000),
        status: faker.datatype.number({ max: 3, min: 0, 
          precision: 1 }),
      });
    }
    

我们创建了两个列表,activitiesopportunities,并使用 faker.js 库用随机数据填充这些列表。

  1. 接下来,创建这两个方法:

    const listOpportunities = (req: Request, res: Response) => {
      const { slice } = req.query;
      res.send({
        data: opportunity.slice(0, slice ? Number(slice) :
          undefined),
        success: true,
      });
    };
    const getOpportunity = (req: Request, res: Response) => {
      const { opportunityId } = req.query;
      res.send(opportunity[Number(opportunityId)]);
    };
    

listOpportunities 方法使用 slice 请求查询参数中给出的数字来切片 opportunities 数组。

getOpportunity 方法通过 opportunityId 请求查询参数提供的索引位置访问 opportunity 数组项。

  1. 最后,创建模拟端点的定义,如下所示:

    export default {
      '/api/opportunity/list': listOpportunities,
      '/api/opportunity': getOpportunity,
      '/api/opportunity/activities': (_: any, res:
     Response) =>
        res.send({ data: activities, success: true }),
      'POST /api/opportunity': (_: any, res: Response) =>
        res.status(201).send({ success: true }),
      'PUT /api/opportunity/disable': (_: any, res:
     Response) =>
        res.send({ success: true }),
      'PUT /api/opportunity': (_: any, res: Response) =>
        res.send({ success: true }),
    };
    

我们定义了六个端点:

  • 一个用于列出所有机会的模拟端点(/api/opportunity/list

  • 一个通过 ID 获取机会的模拟端点(/api/opportunity

  • 一个用于获取机会活动的模拟端点(/api/opportunity/activities

  • 一个用于创建新机会记录的模拟端点(POST /api/opportunity

  • 一个用于禁用机会记录的模拟端点(PUT /api/opportunity/disable

  • 一个用于更新机会记录的模拟端点 (PUT /api/opportunity)

在本节中,我们使用 faker.js 库创建模拟文件,以提供我们接口的数据。

现在,我们将学习如何使用 services 文件夹来组织我们的项目,并使用 umi-request 库向我们的模拟后端发送请求。

使用 Umi request 发送 HTTP 请求

在本节中,我们将使用 umi-request 库开发对后端的请求。

我们将在 services 文件夹内为每个上下文创建单独的文件来存储所有请求。这种组织方式有助于我们清理组件代码并重复使用接口中的请求。

对于发送 HTTP 请求,我们将使用 Umi request。这是一个基于 fetchaxios 库的简单易用的库,它提供了常见的功能,如错误处理和缓存。以下是一个示例:

request<Product>('/api/products', {
  method: 'POST',
  headers: { Authorization: 'Bearer eyJhbGciOi...' },
  params: { onSale: true },
  data: {
    id: 0,
    title: 'My product',
    price: 10.0,
  },
});

request 函数需要两个主要参数 - 我们想要发送请求的 URL 参数,以及我们可以定义 HTTP 方法、请求头、请求参数和请求体(在 data 属性中)的 options 参数。您还可以确定响应类型。在这个例子中,我们使用 Product 接口描述了响应类型。

按照以下步骤开发请求:

  1. src 文件夹中创建一个名为 services 的新文件夹。

  2. services 文件夹内,创建一个名为 analytics.ts 的新文件,并编写请求,如下所示:

    import { HistoryByMonth, LeadsSource, TopOpportunity, } from '@/types/analytics';
    import { request } from 'umi';
    export function getTopOpportunities() {
      return request<{ data: TopOpportunity[]; 
        success: boolean }>(
        `/api/analytics/top/opportunity`,
        {
          method: 'GET',
        },
      );
    }
    export function getLeadsBySource() {
      return request<{ data: LeadsSource[]; 
        success: boolean }>(
        `/api/analytics/leads/source`,
        {
          method: 'GET',
        },
      );
    }
    export function getHistoryByMonth() {
      return request<{ data: HistoryByMonth[]; 
        success: boolean }>(
        `/api/analytics/bymonth/opportunity`,
        {
          method: 'GET',
        },
      );
    }
    

我们创建了三个函数 - getTopOpportunities 用于请求顶级机会,getLeadsBySource 用于按来源请求线索,以及 getHistoryByMonth 用于按月份请求赢得/失去的机会。

  1. 现在,我们可以在 pages/Reports 文件夹中的 index.tsx 文件上使用请求响应来填充我们的图表数据源:

    const [leadsBySource, setLeadsBySource] = 
      useState<LeadsSource[]>([]);
    const [historyByMonth, setHistoryByMonth] = 
      useState<any[]>([]);
    const [topOpp, setTopOpp] = 
      useState<TopOpportunity[]>([]);
    useEffect(() => {
      const fetchTopOpp = async () => {
        setTopOpp((await getTopOpportunities()).data);
      };
      const fetchLeadsBySource = async () => {
        setLeadsBySource((await getLeadsBySource()).data);
      };
      const fetchHistoryByMonth = async () => {
        setHistoryByMonth((await 
                           getHistoryByMonth()).data);
      };
      fetchHistoryByMonth();
      fetchLeadsBySource();
      fetchTopOpp();
    }, []);
    

我们使用 useState React 钩子创建了三个状态来存储我们的图表数据,并利用 useEffect React 钩子在页面渲染时填充我们的图表。

  1. 按照以下方式从 analytics.ts 文件导入所有 React 依赖和函数:

    import { useState, useEffect } from 'react';
    import {
      getHistoryByMonth,
      getLeadsBySource,
      getTopOpportunities,
    } from '@/services/analytics';
    
  2. 接下来,将每个状态添加到相应的 Chart 组件的 data 属性中。结果应该如下所示:

![Figure 3.1 – 填充数据的报告页面图表]

![Figure 3.01_B18503.jpg]

Figure 3.1 – 填充数据的报告页面图表

  1. 让我们在客户页面创建请求。在 services 文件夹中,创建一个名为 customer.ts 的新文件,并编写请求,如下所示:

    import { Customer } from '@/types/customer';
    import { request } from 'umi';
    export function listCustomers(params?: any) {
      return request<{ data: Customer[]; success: boolean 
        }>(`/api/customer/list`, {
        method: 'GET',
        params,
      });
    }
    export function createCustomer(customer: Customer) {
      return request<{ success: boolean 
        }>(`/api/customer`, {
        method: 'POST',
        data: customer,
      });
    }
    export function disableCustomer(customerId?: string) {
      return request<{ success: boolean 
        }>(`/api/customer/disable`, {
        method: 'PUT',
        params: { customerId },
      });
    }
    export function updateCustomer(customer: Customer) {
      return request<{ success: boolean 
        }>(`/api/customer`, {
        method: 'PUT',
        data: customer,
      });
    }
    

我们创建了四个函数 - listCustomers 用于列出所有客户,createCustomer 用于发布新的客户记录,disableCustomer 用于禁用客户记录,以及 updateCustomer 用于更新客户记录。

  1. 现在,我们可以在客户页面上的 ProTable 组件中列出客户。通过在 pages/Customers 文件夹中的 index.tsx 文件中添加以下行,将 listCustomers 函数导入:

    import { listCustomers } from '@/services/customer';
    
  2. 接下来,将listCustomers函数添加到ProTable组件的request属性中,如下所示:

    request={listCustomers}
    

结果应该如下所示:

![图 3.2 – ProTable 组件中的客户列表图 3.02_B18503.jpg

图 3.2 – ProTable 组件中的客户列表

  1. 最后,我们在机会页面上创建请求。在services文件夹中创建一个名为opportunity.ts的新文件。

  2. opportunity.ts文件中创建以下函数,如下所示:

    import { Opportunity, Activity } from '@/types/opportunity';
    import { request } from 'umi';
    export function listOpportunities(params?: any) {
      return request<{ data: Opportunity[];
        success: boolean }>(
        `/api/opportunity/list`,
        {
          method: 'GET',
          params,
        },
      );
    }
    export function listActivities(params?: any) {
      return request<{ data: Activity[]; 
        success: boolean }>(
        `/api/opportunity/activities`,
        {
          method: 'GET',
          params,
        },
      );
    }
    export function getOpportunity(params?: any) {
      return request<Opportunity>(`/api/opportunity`, {
        method: 'GET',
        params,
      });
    }
    

我们创建了三个函数——listOpportunities用于获取所有机会,listActivities用于列出所有机会活动,以及getOpportunity通过 ID 获取机会。

  1. 接下来,在opportunity.ts文件中创建其他三个函数,如下所示:

    export function createOpportunity(opportunity: Opportunity) {
      return request<{ success: boolean 
        }>(`/api/opportunity`, {
        method: 'POST',
        data: opportunity,
      });
    }
    export function disableOpportunity(opportunityId?: string) {
      return request<{ success: boolean 
        }>(`/api/opportunity/disable`, {
        method: 'PUT',
        params: { opportunityId },
      });
    }
    export function updateOpportunity(opportunity: Opportunity) {
      return request<{ success: boolean 
        }>(`/api/opportunity`, {
        method: 'PUT',
        data: opportunity,
      });
    }
    

我们创建了另外三个函数——createOpportunity用于创建新的机会记录,disableOpportunity用于禁用机会记录,以及updateOpportunity用于更新机会记录。

  1. 现在,我们可以在机会页面的ProTable组件中列出机会。将以下导入添加到pages/Opportunity文件夹中的index.tsx文件中:

    import { listOpportunities } from '@/services/opportunity';
    
  2. 接下来,将listOpportunities函数添加到ProTable组件的request属性中,如下所示:

    request={listOpportunities}
    

结果应该如下所示:

![图 3.3 – ProTable 组件中的机会列表图 3.03_B18503.jpg

图 3.3 – ProTable 组件中的机会列表

  1. 让我们也在机会详情页上获取机会并列出机会活动。将以下导入添加到机会详情页中:

    import { useEffect, useState } from 'react';
    import { getOpportunity, listActivities } from '@/services/opportunity';
    
  2. 接下来,添加存储机会的状态以及请求它的效果,如下所示:

    const [opportunity, setOpportunity] = 
      useState<Opportunity>();
    useEffect(() => {
      const fetchOpportunity = async () => {
        setOpportunity(await getOpportunity({ 
          opportunityId: id }));
      };
      fetchOpportunity();
    }, [])
    
  3. 接下来,将opportunity状态添加到ProDescriptions组件的dataSource属性中,如下所示:

    dataSource={opportunity}
    
  4. 让我们也将listActivities函数添加到ProTable组件的request属性中,如下所示:

    request={listActivities}
    
  5. opportunity status属性添加到Step组件的current属性中,如下所示:

    current={opportunity?.status}
    

现在,机会详情页应该如下所示:

![图 3.4 – 机会 – 详细信息和活动列表图 3.04_B18503.jpg

图 3.4 – 机会 – 详细信息和活动列表

在本节中,我们创建了services文件夹和每个页面使用的 umi-request 库的请求。我们还使用每个页面的请求来访问数据以填充我们的接口。接下来,我们将通过创建模型文件来学习在组件之间共享状态和逻辑。

使用模型来共享状态和逻辑

在本节中,我们将创建组件之间共享状态和逻辑的模型。

模型是一个特殊的自定义 React 钩子,用于集中管理特定上下文的状态和逻辑。

我们必须在src/models文件夹内创建模型文件,并且我们可以使用useModel自定义钩子来访问这些模型,如下所示:

const { currentUser } = useModel('user');

这里,user命名空间与模型文件名匹配,因此模型文件必须命名为user.ts

让我们创建 customer 模型和 opportunity 模型来演示模型的使用。这两个模型将包含创建、读取和更新操作的逻辑和结果,并在不同的接口之间共享这些操作。

按照以下步骤创建模型:

  1. src 文件夹内创建一个名为 models 的新文件夹。

  2. 接下来,在 models 文件夹下创建一个名为 customer.ts 的新文件,并添加以下内容:

    import { useCallback, useState } from 'react';
    import { Customer } from '@/types/customer';
    import {
      disableCustomer,
      updateCustomer,
      createCustomer,
    } from '@/services/customer';
    export interface CustomerModel {
      disable: (customerId: string) => void;
      update: (customer: Customer) => void;
      create: (customer: Customer) => void;
      clearResult: () => void;
      result: { success?: boolean };
    }
    

我们创建了 CustomerModel 接口来描述我们希望在组件之间共享的所有函数和状态。

  1. 现在,创建以下函数和状态:

    export default (): CustomerModel => {
      const [result, setResult] = useState<{
        success?: boolean }>({
        success: false,
      });
      const disable = useCallback(async (
        customerId?: string) => {
        setResult(await disableCustomer(customerId));
      }, []);
      const update = useCallback(async (
        customer: Customer) => {
        setResult(await updateCustomer(customer));
      }, []);
      const create = useCallback(async (
        customer: Customer) => {
        setResult(await createCustomer(customer));
      }, []);
      const clearResult = useCallback(() => setResult({ 
        success: false }), []);
      return { disable, update, create, clearResult, 
        result };
    };
    

我们创建了一个状态来存储结果,并使用 services 文件中的请求来执行操作。

  1. 让我们在 pages/Customer 文件夹中的 index.tsx 文件中使用 customer 模型函数:

    const { disable, update, clearResult, result } = 
      useModel('customer');
      const { formatMessage } = useIntl();
      useEffect(() => {
        if (result?.success) {
          message.success(formatMessage({ 
            id: 'messages.success.operation' }));
          clearResult();
        }
      }, [result]);
    

我们使用 result 状态来确定操作是否成功,并显示成功消息。

  1. 使用 model 函数将 editable 属性添加到 ProTable 组件中,如下所示:

    editable={{
      type: 'multiple',
      deletePopconfirmMessage: <FormattedMessage 
        id="table.confirm" />,
      deleteText: <FormattedMessage id="table.disable" />,
      onDelete: async (key) => disable(key as string),
      onSave: async (_, record) => update(record),
    }}
    

我们使用 disableupdate 函数在 ProTable 组件中提供可编辑功能。

  1. 现在,重复之前的步骤来创建 opportunity 模型,并在 ProTable 组件中启用可编辑功能。

  2. 将文本添加到 locales 文件夹下的 en-US.ts 文件中,如下所示:

    'table.disable': 'Disable',
    'table.confirm': 'Do you want to disable the record?',
    

现在,你可以在两个页面上编辑记录,如下面的截图所示:

功能 3.5 – 客户页面上的 ProTable 可编辑功能

功能 3.5 – 客户页面上的 ProTable 可编辑功能

在本节中,你学习了模型的工作原理。我们创建了 customeropportunity 模型以共享状态和逻辑,并在 ProTable 可编辑功能中使用它们。

摘要

在本章中,我们为所有后端数据创建了定义文件,并在每个页面上创建了 ProTable 列定义。我们使用 ProDescritions 组件和 Activity 接口创建了机会详情页面来描述机会活动。

你学习了如何使用 Umi 模拟文件工作,以及如何通过为我们的应用程序创建模拟文件来创建模拟端点以提供模拟的后端数据和逻辑。接下来,你学习了如何通过为我们的应用程序创建 services 文件来组织应用程序请求,并使用 umi-request 库发送请求。最后,你学习了模型的工作原理,并创建了 customeropportunity 模型以在组件之间共享逻辑和状态。

在下一章中,你将学习如何通过配置 umi-request 库来处理 API 错误响应,使用 plugin-access 保护路由,并在登录后存储和全局访问用户信息。

第二部分:保护、测试和部署 Web 应用

本节旨在教导读者如何通过实施软件测试和整洁的代码风格来构建高质量的应用程序,并最终将其部署到在线服务。读者将学会处理错误、保护路由、学习配置和使用格式化工具、理解并编写软件测试,以及如何在 AWS 上托管应用程序。

本节包含以下章节:

  • 第四章, 错误处理、身份验证和路由保护

  • 第五章, 代码风格和格式化工具

  • 第六章, 前端应用测试

  • 第七章, 单页应用部署

第四章:错误处理、认证和路由保护

我们需要在我们的接口中实现错误处理和安全措施,以确保应用程序的质量和用户体验良好。

在本章中,我们将修改在 第一章 中创建的登录页面,即 环境设置和 UmiJS 简介,并配置我们应用程序的默认 HTML 模板。你将学会如何通过配置应用程序的初始状态来存储和全局访问数据。接下来,你将学会如何使用 Umi 的 plugin-access 来阻止未经授权的访问。最后,你将学会通过配置 Umi 请求来处理 HTTP 错误响应并显示反馈消息。

在本章中,我们将涵盖以下主要主题:

  • 修改登录页面和定义 HTML 模板

  • 存储和全局访问用户信息

  • 根据用户角色保护应用程序路由

  • 处理 HTTP 错误响应

到本章结束时,你将学会如何配置和使用 plugin-initial-state 来在应用程序中全局存储和访问信息。你还将学会如何配置和使用 plugin-access 来保护路由。最后,你将学会通过配置 umi-request 库来处理 HTTP 错误响应。

技术要求

要完成本章的练习,你只需要一台装有任何操作系统(我推荐 Ubuntu 20.04 或更高版本)的计算机,以及 第一章 中安装的软件(VS Code、Node.js 和 Yarn)。

你可以在 GitHub 仓库中找到本章的完整项目,该仓库位于 github.com/PacktPublishing/Enterprise-React-Development-with-UmiJsChapter04 文件夹中。

修改登录页面和定义 HTML 模板

在本节中,我们将创建一个 Umi 模拟文件和请求来模拟用户认证、用户登录页面,并配置我们应用程序的默认 HTML 模板。

让我们从模拟文件开始。我们将创建登录、登出和获取用户信息的端点。按照以下步骤创建文件:

  1. mock 文件夹中创建一个名为 user.ts 的新文件。接下来,创建 login 函数如下:

    import { User } from '@/types/user.d';
    import { Request, Response } from 'express';
    const user: { currentUser: User } = {
      currentUser: {
        isLoggedIn: false,
      },
    };
    const login = (req: Request, res: Response) => {
      const { email, password } = req.body;
    };
    
  2. 将以下 if 语句添加到 login 函数中:

      if (email == 'john@doe.com' && password == 'user') {
        user.currentUser = {
          id: 0,
          name: 'John Doe',
          company: 'Umi Group',
          role: {
            id: 1,
            title: 'Inside Sales',
          },
          isLoggedIn: true,
        };
        res.json(user.currentUser);
      }
    

在这里,我们定义了一个条件,允许内部销售代表 John Doe(模拟用户)访问应用程序。用户角色将决定用户可以执行哪些操作以及他们可以访问哪些页面。

  1. 接下来,将以下 else ifelse 语句添加到 login 函数中:

    else if (email == 'marry@doe.com' && 
             password == 'admin') {
        user.currentUser = {
          id: 1,
          name: 'Marry Doe',
          company: 'Umi Group',
          role: {
            id: 0,
            title: 'Sales Manager',
          },
          isLoggedIn: true,
        };
        res.json(user.currentUser);
      } else {
        res.status(401).send();
      }
    

在这里,我们定义了一个条件,允许模拟用户 Mary Doe,销售经理访问应用程序。我们还确定,如果用户不是 John Doe 或 Marry Doe,模拟 API 将返回 HTTP 401 错误,这是未认证的状态码。

  1. 最后,按照以下方式将其他功能和端点路由定义添加到user.ts文件中:

    const logout = (_: any, res: Response) => {
      user.currentUser = { isLoggedIn: false };
      res.send({ success: true });
    };
    const getUser = (_: any, res: Response) => {
      if (!user.currentUser.isLoggedIn) {
        res.status(204).send();
      } else {
        res.json(user.currentUser);
      }
    };
    export default {
      'POST /api/login': login,
      'POST /api/logout': logout,
      '/api/currentUser': getUser,
    };
    

我们创建了模拟登出和获取已登录用户信息的函数。

现在,我们需要在services文件夹中创建获取用户信息、登录和登出应用程序的请求。按照以下步骤创建请求:

  1. src文件夹下的services文件夹中创建一个名为user.ts的新文件。

  2. 将以下请求添加到user.ts文件中:

    import { User } from '@/types/user.d';
    import { request } from 'umi';
    export function getCurrentUser() {
      return request<User>(`/api/currentUser`, {
        method: 'GET',
      });
    }
    export function userLogin(email: string, 
      password: string) {
      return request<User>(`/api/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        data: { email, password },
      });
    }
    export function userLogout() {
      return request<void>(`/api/logout`, {
        method: 'POST',
      });
    }
    

我们创建了请求来访问在user.ts模拟文件中定义的端点。

我们创建了一个 Umi 模拟文件来模拟用户服务和后端请求。现在,我们将创建一个登录页面,用户可以输入他们的电子邮件和密码并在应用程序中进行身份验证。

修改登录页面

我们需要一个登录页面,用户可以使用他们的电子邮件和密码登录。我们已经在第一章环境设置和 UmiJS 简介中使用了 Umi UI 创建了一个登录页面,所以我们只需要调整页面组件。按照以下步骤调整登录页面以匹配我们的主题:

  1. 按照以下方式重构pages/Login文件夹中的index.tsx文件:

    import { SelectLang, useModel, history } from 'umi';
    import styles from './index.less';
    import LoginForm from './LoginForm';
    export default function Page() {
      return (
        <div>
          <span className={styles.header}>
            <span className={styles.logo}>
              <img
                height={45}
                alt="crm logo"
                src="https://img.icons8.com/ios-filled/
                     50/ffffff/customer-insight.png"
              />
              &nbsp; &nbsp;
              <h1 className={styles.title}>Umi CRM</h1>
            </span>
            <SelectLang className={styles.language} />
          </span>
          <div className={styles.container}>
            <LoginForm />
          </div>
        </div>
      );
    }
    

我们创建了一个页面页眉来显示我们的应用程序标志和语言选择器。

  1. 现在,按照以下方式在index.less文件中添加 CSS 类来设置标题、语言选择器和登录表单容器的样式:

    @import '~antd/es/style/themes/default.less';
    .title {
      text-align: center;
    }
    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    .language {
      color: white;
    }
    
  2. 接下来,按照以下方式在index.less文件中添加 CSS 类来设置页眉和标志的样式:

    .header {
      display: flex;
      flex-flow: row nowrap;
      justify-content: space-between;
      padding: 10px;
      margin-bottom: 20px;
      background: #1895bb;
      background: linear-gradient(50deg, #1895bb 0%,
                                  #14cfbd 100%);
      > .logo {
        width: 95%;
        display: flex;
        flex-flow: row nowrap;
        justify-content: center;
        > h1 {
          color: white;
        }
      }
    }
    
  3. 让我们也在LoginForm组件样式中做一些更改。按照以下方式重构LoginForm文件夹中的index.less文件:

    @import '~antd/es/style/themes/default.less';
    .container {
      :global {
        #components-form-demo-normal-login .login-form {
          width: 450px;
          margin: 5%;
          @media screen and (max-width: @screen-sm) {
            width: 90%;
          }
        }
        #components-form-demo-normal-login 
          .login-form-forgot {
          float: right;
        }
        #components-form-demo-normal-login 
          .login-form-button {
          width: 100%;
        }
      }
    }
    

我们修改了表单的widthmargin,并在小屏幕上使用默认 Ant Design 变量的@screen-sm断点将width定义为100%

这些都是在登录页面上的所有更改。结果应该看起来像以下这样:

图 4.1 – 应用主题的登录页面

图 4.1 – 应用主题的登录页面

图 4.1 – 应用主题的登录页面

如果你使用移动设备访问我们的应用程序,你会注意到它似乎不太对劲,尽管我们已经开发了一个完全响应式的登录页面。我们将通过定义应用程序的默认模板来学习如何解决这个问题。

定义默认 HTML 模板

如果你熟悉开发响应式网站,你会知道我们应用程序页面的问题是移动设备上的viewport缩放。我们需要在每个应用程序页面上提供一个带有正确 viewport 属性的 HTML meta 标签来解决此问题。正如你所知道的那样,我们的应用程序是一个单页应用程序SPA),所以我们只需要修改一个 HTML 文档。

Umi 为我们提供了自定义应用程序默认 HTML 模板的选项,即document.ejs文件。如果src/pages文件夹中存在名为document.ejs的文件,Umi 将使用它作为默认 HTML 文档。

您也可以在document.ejs文件中使用context.config变量访问应用程序配置。以下是一个示例:

<!doctype html>
<html>
<head>
  <title>
    <%= context.config.layout.title %>
  </title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

在此示例中,我们将 HTML 标题标签的内容定义为config/config.ts文件中存在的layout.title配置。

让我们为我们的应用程序创建默认的 HTML 模板。

src/pages文件夹中创建一个名为document.ejs的新文件,并创建模板,如下所示:

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,
    initial-scale=1.0" />
  <title>Umi CRM</title>
</head>
<body style="background-color: whitesmoke;">
  <div id="root"></div>
</body>
</html>

我们将视口缩放设置为1.0并将内容宽度设置为与设备屏幕宽度相同。

以下截图显示了在移动设备上带有视口 meta 标签的登录页面和没有视口 meta 标签的登录页面之间的差异:

![图 4.2 – 没有视口缩放的登录页面(左侧)和有视口缩放的登录页面(右侧)]

](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/entp-rct-dev-umijs/img/Figure_4.02_B18503.jpg)

图 4.2 – 没有视口缩放的登录页面(左侧)和有视口缩放的登录页面(右侧)

在本节中,我们创建了一个 Umi 模拟文件和请求来模拟用户认证。我们还修改了登录页面,并定义了视口缩放,通过创建应用程序的默认 HTML 模板来正确显示移动设备上的应用程序页面。

在下一节中,我们将学习如何在用户登录后存储和全局访问用户信息。

存储和全局访问用户信息

在本节中,我们将配置plugin-initial-state插件来存储和全局访问用户信息。

要配置初始状态,我们只需要在app.tsx文件中创建一个名为getInitialState的函数。getInitialState函数将在 React 渲染整个应用程序之前执行,其返回值将用作全局状态。我们可以使用@@initialState模型来访问这些值。

让我们按照以下步骤配置初始状态:

  1. src文件夹中创建一个名为globalState.d.ts的新文件,并创建GlobalState接口,如下所示:

    import { User } from '@/types/user.d';
    export interface GlobalState {
      login?: (email: string, password: string) => 
        Promise<User>;
      logout?: () => Promise<void>;
      fetchUser?: () => Promise<User>;
      currentUser?: User;
    }
    
  2. src文件夹中的app.tsx文件中创建getInitialState函数,如下所示:

    import routes from '../config/routes';
    import { RunTimeLayoutConfig, history } from 'umi';
    import HeaderOptions from './components/HeaderOptions';
    import { getCurrentUser, userLogin, userLogout } from './services/user';
    import { GlobalState } from './types/globalState';
    export async function getInitialState(): 
      Promise<GlobalState> {
      const fetchUser = async () => 
        await getCurrentUser();
      const logout = async () => {
        await userLogout(), history.push('/login');
      };
      const login = 
        async (email: string, password: string) => {
        return await userLogin(email, password);
      };
      const currentUser = await fetchUser();
      return {
        login,
        logout,
        fetchUser,
        currentUser,
      };
    }
    

在前面的代码块中,我们创建了登录、注销、获取用户数据并将其作为初始状态值返回的函数。

现在,我们可以通过读取currentUser属性来访问用户信息。

接下来,让我们按照以下步骤在布局标题中读取初始状态:

  1. src/components文件夹下的index.tsx文件中,按照以下步骤读取HeaderMenu组件的初始状态:

    export default function HeaderMenu() {
      const { initialState, setInitialState } = 
        useModel('@@initialState');
      const userLogout = () => {
        initialState?.logout?.();
        setInitialState((state) => ({
          ...state,
          currentUser: undefined,
        }));
      };
    

我们创建了userLogout函数来注销并将currentUser状态设置为undefined

  1. 现在,在Menu组件中,添加onClick事件,当用户点击注销菜单项时执行userLogout函数,如下所示:

    const options = (
        <Menu className=
          {styles.menu} onClick={userLogout}>
          <Menu.Item key="center">
            <LogoutOutlined /> Logout
          </Menu.Item>
        </Menu>
      );
    
  2. 最后,在Avatar组件下方添加用户的姓名,如下所示:

    return (
        <Dropdown className=
          {styles.dropdown} overlay={options}>
          <span>
            <Avatar size="small" className=
              {styles.avatar} icon={<UserOutlined />} />
            <span className={`${styles.name} anticon`}>
              {initialState?.currentUser?.name}
            </span>
          </span>
        </Dropdown>
      );
    

接下来,让我们按照以下步骤在主页上读取用户信息:

  1. pages/Home文件夹下的index.tsx文件中,按照以下方式读取初始状态:

    const { initialState } = useModel('@@initialState');
    
  2. 接下来,按照以下步骤添加用户的姓名、角色和公司:

    <div className={styles.content}>
      <div className={styles.contentTitle}>
        <FormattedMessage id="greetings.hello" /> 
          {initialState?.currentUser?.name},{' '}
        <FormattedMessage id="greetings.welcome" />.
      </div>
      <div>
        {initialState?.currentUser?.role?.title} |{' '}
        {initialState?.currentUser?.company}
      </div>
    </div>
    

我们还需要在登录页面上执行login函数。按照以下步骤来开发登录流程:

  1. 将以下函数添加到pages/Login/LoginForm文件夹中的index.tsx文件:

    const { initialState, setInitialState } = useModel('@@initialState');
      const onFinish = async (values: any) => {
        const user = await initialState?.login?.(
          values.username, values.password);
        if (user) {
          setInitialState((state) => ({
            ...state,
            currentUser: user,
          }));
        }
      };
    

当用户提交登录表单时,我们执行login函数,如果登录成功,我们将用户信息保存到初始状态。

  1. 现在,将以下 React 效果添加到pages/Login文件夹中的index.tsx文件:

    const { initialState } = useModel('@@initialState');
      useEffect(() => {
        if (initialState?.currentUser?.isLoggedIn)
          history.push('/');
      }, [initialState?.currentUser]); 
    

在这里,我们定义了当currentUser状态改变时,如果登录成功,则将用户重定向到主页。

当用户登录应用程序时,我们将他们重定向到主页,但当他们注销且不再有权访问其他页面时,我们需要将用户转回到登录页面。我们可以通过在布局运行时配置中读取初始状态来设置这种行为。

将以下行添加到app.tsx文件中的layout配置的onPageChange函数:

export const layout: RunTimeLayoutConfig = ({ initialState }) => {
  return {
    routes,
    rightContentRender: () => <HeaderOptions />,
    onPageChange: () => {
      const isLoggedIn =
      initialState?.currentUser?.isLoggedIn;
      const location = history.location.pathname;
      if (!isLoggedIn && location != '/login') 
        history.push(`/login`);
    },
  };
};

在这里,我们定义了如果用户未登录且当前页面不是登录页面,则将用户重定向到登录页面的逻辑。

在本节中,我们配置了应用程序的初始状态,在主页和MenuHeader组件中读取用户信息,并通过在布局配置和登录页面中添加一些行来设置登录流程。

在下一节中,我们将学习如何使用plugin-access来阻止未授权访问。

根据用户角色保护应用程序路由

在本节中,我们将配置 Umi 的plugin-access插件来定义用户权限并保护路由和功能免受未授权访问。

要配置访问插件,我们必须在src文件夹中创建一个access.ts文件。access.ts文件必须导出一个返回对象的函数,并且该对象的每个属性都必须是一个表示权限的布尔值。考虑以下示例:

export default function (initialState: any) {
  const { access } = initialState;
  return {
    readOnly: access == 'basic',
  };
}

在这个例子中,我们从初始状态中读取access属性,如果access等于basic,则返回readOnly: true权限。

让我们为我们的应用程序创建一个access.ts文件。

src文件夹中创建一个名为access.ts的新文件,并创建如下所示的default函数:

import { GlobalState } from './types/globalState';
export default function (initialState: GlobalState) {
  const { currentUser } = initialState;
  return {
    canAdmin: currentUser?.role?.id == 0,
  };
}

在前面的代码块中,我们定义了role id等于0(销售经理)的用户作为应用程序管理员。

现在,为了演示如何使用canAdmin权限,让我们按照以下步骤创建一个只有管理员可以访问的新页面:

  1. 通过运行以下命令在pages文件夹中创建一个新页面:

    yarn umi g page /Workflow/index --typescript --less
    
  2. index.tsx文件中,按照以下方式添加ProTable组件:

    import ProTable from '@ant-design/pro-table';
    import { Button } from 'antd';
    import { PageContainer } from '@ant-design/pro-layout';
    import { PlusOutlined } from '@ant-design/icons';
    import { FormattedMessage } from '@/.umi/plugin-locale/localeExports';
    import columns from './columns';
    export default function Page() {
      return (
        <PageContainer>
          <ProTable<any>
            columns={columns}
            dataSource={workflow}
            rowKey="id"
            search={false}
            pagination={{ pageSize: 5 }}
            dateFormatter="string"
            toolBarRender={() => [
              <Button key="button" icon={<PlusOutlined />}
               type="primary">
                <FormattedMessage id="table.new" />
              </Button>,
            ]}
          />
        </PageContainer>
      );
    }
    

我们创建了一个简单的ProTable组件来列出工作流程配置。

  1. 接下来,按照以下步骤添加数据源以填充ProTable

    const workflow = [
      {
        id: 0,
        name: 'AssignEmail',
        table: 'Opportunity',
        type: 0,
        trigger: 0,
      },
      {
        id: 1,
        name: 'NewOpportunity',
        table: 'Analytics',
        type: 1,
        trigger: 1,
      },
    ];
    
  2. pages/Workflow文件夹中,创建一个名为columns.tsx的新文件,并添加以下列定义:

    import { ProColumns } from '@ant-design/pro-table';
    import { FormattedMessage } from 'umi';
    const columns: ProColumns<any>[] = [
      {
        title: <FormattedMessage id="table.workflow.name"
               />,
        dataIndex: 'name',
      },
      {
        title: <FormattedMessage id="table.workflow.type"
               />,
        dataIndex: 'type',
      },
      {
        title: <FormattedMessage id="table.workflow.table"
               />,
        dataIndex: 'table',
      },
      {
        title: <FormattedMessage id="table.options" />,
        valueType: 'option',
        hideInSetting: true,
        hideInDescriptions: true,
        render: () => [
          <a>
            <FormattedMessage id="table.edit" />
          </a>,
        ],
      },
    ];
    export default columns;
    
  3. 将以下文本添加到locales文件夹中的en-US.ts文件:

    'table.workflow.name': 'Name',
    'table.workflow.type': 'Type',
    'table.workflow.table': 'Table',
    
  4. 现在,将工作流程页面的路由配置添加到routes.ts文件中,如下所示:

    {
      path: '/workflow',
      name: 'workflow',
      access: 'canAdmin',
      icon: 'DeploymentUnitOutlined',
      component: '@/pages/Workflow',
    },
    

注意路由配置中的access属性。在access属性中,我们可以设置在access.ts文件中定义的权限。现在,只有具有销售经理角色的用户才能访问工作流程页面。

  1. 我们也可以在布局配置中定义一个默认页面,当用户没有足够的权限访问页面时显示。将以下定义添加到app.tsx文件中的布局配置中:

    unAccessible: (
      <Result
        status="403"
        title="403"
        subTitle="Sorry, you are not authorized to access 
                  this page."
        extra={
            <Button type="primary" onClick={() => 
              history.push('/')}>
              Back to Home
            </Button>
        }
      />
    )
    

我们添加了 Ant Design 的Result组件来显示未授权错误页面和按钮,以便用户可以返回主页。以下是页面的外观:

Figure 4.3 – Unauthorized error page

图 4.3 – 未授权错误页面

现在我们已经创建了access.ts文件,并使用canAdmin权限保护工作流程页面。接下来,我们将学习如何使用权限来保护其他应用程序功能。

使用useAccess钩子

我们可以使用在access.ts文件中创建的权限,通过使用useAccess钩子和Access组件来授权用户在我们的应用程序中执行任何操作。考虑以下示例:

import { useAccess } from "umi";
const Page = (props) => {
  const [disabled, setDisabled] = useState<any>();
  const access = useAccess();
  if (access.readOnly) {
    setDisabled(true);
  }
  return <Button disabled={disabled}> Edit </Button>;
};
export default Page;

在此示例中,我们读取readOnly权限来定义编辑按钮是否将被禁用。

现在,考虑另一个使用Access组件的示例:

import { useAccess } from "umi";
const Page = (props) => {
  const access = useAccess();
  return (
    <Access
      accessible={access.readAndWrite}
      fallback={<div>You are not allowed to write 
                content.</div>}
    >
      <TextArea></TextArea>
    </Access>
  );
};
export default Page;

在此示例中,如果用户没有readAndWrite权限,我们将渲染fallback属性中的内容,而不是渲染TextArea组件。

让我们使用useAccess钩子,按照以下步骤允许管理员将机会分配给内部销售代表:

  1. pages/Opportunities文件夹中的index.tsx文件中添加以下行以读取canAdmin权限:

    const { canAdmin } = useAccess();
    
  2. 接下来,将以下属性添加到ProTable组件中:

    rowSelection={canAdmin && { onChange: () => {} }}
    tableAlertOptionRender={() => <a>Assign</a>}
    

我们定义了只有当用户拥有canAdmin权限时,我们才会应用onChange事件,启用ProTable行选择。

现在,如果用户是管理员,他们可以像以下截图所示分配机会:

Figure 4.4 – Assign opportunity feature

图 4.4 – 分配机会功能

在本节中,我们创建了access.ts文件,并基于用户角色定义了管理员权限。然后,我们使用canAdmin权限阻止对工作流程页面和行选择功能的未授权访问。

在下一节中,你将学习如何通过配置umi-request库来处理 HTTP 错误响应。

处理 HTTP 错误响应

在本节中,我们将配置umi-request库以处理错误响应并显示视觉反馈。

我们将使用errorHandler函数,这是 umi-request 库的许多配置之一。我建议你阅读在github.com/umijs/umi-request上可用的文档,以了解更多其他功能。

每当 umi-request 库接收到 HTTP 错误响应时,它将触发errorHandler函数,我们将读取响应状态并向用户显示消息,告知他们他们尝试执行的操作为何失败。

按照以下步骤配置 umi-request 库:

  1. app.tsx文件中,创建一个新函数并添加以下request配置:

    const errorHandler = (error: ResponseError) => {
      const { response } = error;
      let messages = undefined;
      switch (getLocale()) {
        case 'en-US':
          messages = eng;
          break;
        case 'pt-BR':
          messages = port;
          break;
      }
      if (response) {
        message.error(messages[response.status]);
      } else if (!response) {
        message.error(messages['empty']);
      }
      throw error;
    };
    export const request: RequestConfig = { errorHandler };
    

我们使用了 Umi 的getLocale()函数来定义我们将以何种语言显示消息。接下来,我们根据响应状态或空响应显示错误消息,并使用errorHandler函数导出请求配置。

  1. 接下来,我们需要定义消息。在src/locales文件夹下的en-US文件夹中,创建一个名为http.ts的新文件,并添加以下消息:

    export default {
      400: 'The request failed.',
      401: 'Invalid credentials, you are not 
            authenticated.',
      403: 'You cannot perform this operation.',
      404: 'Resource not found.',
      405: 'Operation not allowed.',
      406: 'The operation cannot be completed.',
      410: 'The service is no longer available',
      422: 'Could not process your request.',
      500: 'Internal error, contact administrator.',
      502: 'Internal service communication failed.',
      503: 'Service temporarily unavailable.',
      504: 'The maximum wait time for an answer has 
            expired.',
      empty: 'Failed to connect to services',
    };
    

你还需要下载本书 GitHub 仓库中可用的http.ts文件的葡萄牙语版本,并将其放置在locales/pt-BR文件夹中。

  1. 现在,按照以下方式在app.ts文件中导入en-USpt-BR文件夹中的http.ts文件:

    import eng from './locales/en-US/http';
    import port from './locales/pt-BR/http';
    

当 Umi 请求接收到 HTTP 错误响应时,用户将看到如下截图所示的消息:

![图 4.5 – 失败请求的反馈消息

![img/Figure_4.05_B18503.jpg]

图 4.5 – 失败请求的反馈消息

在本节中,我们配置了 umi-request 库来处理 HTTP 错误响应,并向用户显示反馈消息以告知发生了什么。

摘要

在本章中,我们创建了登录页面和document.ejs文件,并学习了如何设置视口缩放以正确显示我们的移动设备页面。你学习了如何通过配置初始状态插件和读取登录和主页上的初始状态属性来存储和全局访问数据。

我们通过配置访问插件创建了用户权限,并在使用访问插件创建的工作流程页面上阻止了未经授权的访问。我们仅通过访问插件为授权用户启用了ProTable行选择功能。

最后,我们配置了 umi-request 库来处理 HTTP 错误响应,并向用户显示反馈消息以告知发生了什么。

在下一章中,你将学习关于代码风格、格式化以及如何使用linters和格式化工具来改进你的代码。

第五章:代码风格和格式化工具

除了满足业务需求外,一个专业的前端项目还应具备易于维护和扩展的干净源代码。

在本章中,我们将讨论代码风格和一致性。接下来,您将学习如何使用PrettierEditorConfig在拥有多个成员且使用各种集成开发环境IDEs)和编辑器的团队中强制执行标准的代码格式。最后,我们将把ESLint添加到我们的项目中,并配置它与 Prettier 一起工作以提高代码质量。

在本章中,我们将涵盖以下主要主题:

  • 理解代码风格和一致性

  • 与 EditorConfig 和 Prettier 协作

  • 配置 ESLint 和 Prettier

到本章结束时,您将学会如何配置 Prettier 和 EditorConfig,避免冲突和冗余。您还将学会如何配置 ESLint 来提高代码质量,并使用 Prettier 在同一个项目中格式化代码,避免这两个工具之间的冲突。

技术要求

要完成本章的练习,您只需要一台安装了任何操作系统(我推荐 Ubuntu 20.04 或更高版本)的计算机以及第一章**,环境设置和 UmiJS 简介(VS Code、Node.js 和 Yarn)的软件。

您可以在 GitHub 仓库中找到完整的项目,该仓库位于github.com/PacktPublishing/Enterprise-React-Development-with-UmiJsChapter05文件夹中。

理解代码风格和一致性

在本节中,我们将通过一些示例讨论代码风格,这样您就能理解为什么在处理大型企业项目时使用PrettierEditorConfigESLint等工具是至关重要的。

我们将不会讨论 JavaScript 代码约定,但如果您想复习这个主题,我建议您阅读developer.mozilla.org/en-US/docs/MDN/Guidelines/Code_guidelines/JavaScript上的Mozilla 开发者网络 JavaScript 指南

每位开发者在决定如何格式化代码时都有自己的偏好。即使遵循特定的语言约定,关于代码格式的某些决定也可能使开发者产生分歧。考虑以下函数调用示例:

function execute(param1, param2, param3) {
    return param1 + param2 + param3;
}
execute(arg1, arg2, arg3);

在这里,我们通过传递参数的方式调用函数。在某些情况下,当传递更多参数时,您可能需要分解函数调用,并且您可以通过不同的方式来完成。考虑以下示例:

图 5.1 – 以三种不同的风格分解函数调用

图 5.1 – 以三种不同的风格分解函数调用

在这里,我们以三种不同的风格分解了相同的函数调用:紧贴最后一个括号、对齐括号和对齐参数。现在想象一下,第一个参数又是一个函数调用;复杂性开始增加。考虑另一个例子:

Figure 5.2 – Breaking down functions and inner functions

Figure 5.02 – Breaking down functions and inner functions

图 5.2 – 分解函数和内部函数

在这里,我们使用三种不同的代码风格分解了函数和内部函数调用。你可能已经注意到,每种方法都极大地改变了代码风格。随着更多开发者参与代码并使用不同的风格,代码库将变得不整洁、不专业且难以阅读。

一些风格决策也可能使代码更难以理解。考虑以下例子:

Figure 5.3 – Conditional ternary operator with and without parentheses

Figure 5.03 – Breaking down functions and inner functions

图 5.3 – 带括号和不带括号的条件三元运算符

在这里,我们使用了两种不同的风格来使用条件三元运算符:不带括号和带括号包围。在复杂条件下使用括号可以使代码更容易阅读和理解。

当与大型项目和多个团队成员一起工作时,专业的方法是定义一个标准代码风格,每个开发者都必须遵循。代码风格应该被讨论和记录,以便每个开发者都知道如何使用它。然而,这种方法也引入了其他挑战,因为我们需要确保所有开发者都遵循代码风格。可能的结果是你将不得不审查代码以修复代码风格问题,这既浪费时间又浪费金钱,因为它没有为客户带来价值。

为了避免花费时间审查代码只是为了修复代码风格问题,我们可以使用强制代码风格的格式化工具。

你可以使用众多工具来强制 JavaScript 项目的代码风格和一致性。在接下来的章节中,我们将重点关注三个解决前面提到的问题的工具。我们将查看以下三个工具:

  • Prettier,一个可以解析和格式化 JavaScript、LESS 样式表、JSX 组件等格式的格式化工具

  • EditorConfig,一个强制默认代码格式的工具,几乎任何 IDE 都可以遵循

  • ESLint,一个用于格式化、修复代码和查找质量相关问题的工具

在本节中,我们通过查看示例并理解为什么在多个团队成员的大型项目中实施工具和策略来强制一致的代码风格时需要实施工具和策略。

现在,让我们更仔细地看看 Prettier 和 EditorConfig,看看这两个工具如何协同工作来解决代码风格问题。

使用 EditorConfig 和 Prettier

在本节中,你将了解PrettierEditorConfig如何协同工作以在 IDEs 和开发者的代码中强制执行代码风格,以及如何防止配置这些工具时的冗余。

让我们从学习 EditorConfig 的工作原理开始。

与 EditorConfig 一起工作

EditorConfig 由一个格式文件和一组插件组成,确保几乎任何 IDE 或编辑器都能遵循你在输入时定义的代码风格。在某些情况下,你甚至不需要安装任何扩展,因为各种 IDE 和编辑器都自带对 EditorConfig 的原生支持。你可以在editorconfig.org/了解更多关于 EditorConfig 的信息。

让我们以umi-app模板中包含的格式文件为例,我们使用它从头开始启动我们的项目:

.editorconfig

root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

在我们的项目中,我们使用了以下选项:

  • root: 我们使用此选项告诉 EditorConfig 我们处于root文件夹中,并且它不需要在任意其他文件夹中搜索.editorconfig文件。

  • [*]: 我们使用这个通配符模式将以下选项应用于项目中的每个文件。

  • indent_style: 我们使用此选项来定义使用空格而不是制表符进行缩进。

  • indent_size: 我们使用此选项来定义缩进为两个空格。

  • end_of_line: 此选项设置我们想要用于标记行断的控制字符。我们选择换行符LF)字符(0x0A)。

默认的控制字符可能因 IDE 或编辑器而异。此外,IDEs 不会渲染这类字符,因此使用此选项确保跨 IDE 的一致性至关重要。

  • charset: 我们使用此选项来定义字符集为utf-8

  • trim_trailing_whitespace: 我们将此选项设置为true以删除新行之前的任何空白字符。

  • insert_final_newline: 我们使用此选项来确保所有文件都以新行结束。

  • [*.md]: 我们使用这个通配符模式将以下选项应用于README.md文件以及其他使用 Markdown 定义的文档。

  • trim_trailing_whitespace: 我们将此选项设置为false以确保在新行之前存在空白字符。

  • [Makefile]: 我们使用这个通配符模式将以下选项应用于项目中存在的任何Makefile

我们通常在Makefile中创建命令和逻辑,并使用make实用程序来构建和编译应用程序。

你可以在以下链接中了解更多关于这个工具的信息:www.gnu.org/software/make/.

  • indent_style: 在这里,我们使用此选项来定义使用制表符进行缩进。

如你所注意到的,我们可以使用 EditorConfig 控制代码风格的每个关键方面,并为我们项目中的每种资源类型自定义格式化。所有这些配置都在 IDEs 和编辑器中工作,以确保代码风格的一致性,无论开发者的偏好如何。

接下来,我们将看到另一个与 EditorConfig 配合得很好的工具 Prettier,以强制执行代码风格和一致性。

与 Prettier 一起工作

Prettier 是一个支持众多 JavaScript 框架、样式表扩展、标记语言和配置文件的代码格式化工具。

正如你所知,我们自从第一章**,环境设置和 UmiJS 简介以来一直在使用 Prettier。我们配置了 VS Code 在保存和粘贴时使用 Prettier 格式化代码。

在我们的项目中,EditorConfig 和 Prettier 共同承担着强制执行代码格式的责任,但采用的方法不同。当 EditorConfig 覆盖 IDE 或编辑器的代码风格并确保代码在输入时正确格式化时,Prettier 在开发者输入代码后应用标准代码风格,用 Prettier 定义的标准化代码风格替换原有风格。

当使用 Prettier 时,开发者不需要担心遵循特定的代码风格;他们可以专注于定义接口和开发业务规则。Prettier 将使用一致的代码风格格式化代码,并且几乎不再需要团队就代码风格进行辩论。

虽然 Prettier 不需要很多配置,但在 .prettierrc 文件中我们可以定义一些选项。让我们更仔细地看看我们项目的配置:

.prettierrc

{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80,
  "overrides": [
    {
      "files": ".prettierrc",
      "options": { "parser": "json" }
    }
  ]
}

这些是 .prettierrc 文件中定义的选项:

  • singleQuote:我们使用这个选项在我们的代码中使用单引号而不是双引号,除非在编写 JSX 文件时,该选项被跳过并使用双引号。

  • trailingComma:我们使用这个选项在可能的情况下在我们的代码中打印尾随逗号。

  • printWidth:我们使用这个选项来定义 Prettier 在断行之前的最大行长度。

  • overrides:这个选项类似于 EditorConfig 中的通配符模式。在这个选项中,我们可以覆盖特定文件的选项。在这里,我们配置了 Prettier 专门为 .prettierrc 文件使用 JSON parser

你可以在 .prettierrc 文件中设置的选项有限,因为 Prettier 强制执行其标准代码风格。你可以在 Prettier 文档中找到其他选项,网址为 prettier.io/docs/en/options.html

当在同一个项目中使用 Prettier 和 EditorConfig 时,你需要避免在这两个工具之间设置冗余的选项。一种好的方法是只将相关选项放入 .editorconfig 文件中,以覆盖 IDE 和编辑器的代码风格,并确保你不在 .prettierrc 文件中重复这些选项。

你可以看到,在我们的项目中,EditorConfig 和 Prettier 的所有配置都不同。

Prettier 将解析 .editorconfig 文件以遵循其配置进行代码格式化。由于 IDE 已经根据 EditorConfig 规则格式化了代码,因此 Prettier 可以跳过这些规则并应用自己的代码风格规则。

在本节中,我们学习了如何通过在 .editorconfig 文件中定义代码样式规则来配置 EditorConfig,通过在 .prettierrc 文件中定义规则来配置 Prettier。我们还学习了如何在使用这些工具时避免冗余。

接下来,我们将向项目中添加 ESLint,这是一个补充 EditorConfig 和 Prettier 以提高代码质量的必要工具。

配置 ESLint 和 Prettier

在本节中,我们将配置 ESLint 并将 Prettier 与 ESLint 集成以提高代码质量,并防止这两个工具之间的冲突。

ESLint 是一个用于分析、修复和报告可能导致代码中产生错误的代码不一致性和问题的工具。此工具可以通过实现满足项目需求规则的插件来格式化和提高代码质量。您可以在 eslint.org/ 上了解更多关于 ESLint 的信息。

与 Prettier 和 EditorConfig 一样,ESLint 也将样式规则应用于代码。在我们的场景中,我们使用 EditorConfig 来覆盖 IDE 代码样式,使用 Prettier 通过应用其自己的规则来强制执行一致的代码样式,我们将仅使用 ESLint 提供的代码质量规则。我们可以仅使用 ESLint 进行代码质量和格式化,但 Prettier 在代码格式化方面表现卓越,并且可以轻松与 ESLint 集成。

在深入了解将 Prettier 和 ESLint 集成的细节之前,让我们按照以下步骤安装和配置 ESLint:

  1. 通过运行以下命令安装 ESLint:

    yarn add eslint -D
    
  2. 接下来,运行以下命令来配置 ESLint:

    yarn create @eslint/config
    
  3. 对于第一个问题,您希望如何使用 ESLint?,在终端中选择 检查语法并查找问题 选项并按 Enter 键:

![图 5.4 – ESLint 配置 – 您希望如何使用 ESLint?图 5.04_B18503.jpg

图 5.4 – ESLint 配置 – 您希望如何使用 ESLint?

  1. 对于第二个问题,您的项目使用什么类型的模块?,在终端中选择 JavaScript 模块(import/export) 选项并按 Enter 键:

![图 5.5 – ESLint 配置 – 您的项目使用什么类型的模块?图 5.05_B18503.jpg

图 5.5 – ESLint 配置 – 您的项目使用什么类型的模块?

  1. 对于第三个问题,您的项目使用哪个框架?,在终端中选择 React 选项并按 Enter 键:

![图 5.6 – ESLint 配置 – 您的项目使用哪个框架?图 5.06_B18503.jpg

图 5.6 – ESLint 配置 – 您的项目使用哪个框架?

  1. 对于第四个问题,您的项目是否使用 TypeScript?,选择 选项并按 Enter 键:

![图 5.7 – ESLint 配置 – 您的项目是否使用 TypeScript?图 5.07_B18503.jpg

图 5.7 – ESLint 配置 – 您的项目是否使用 TypeScript?

  1. 对于第五个问题,您的代码在哪里运行?,选择 浏览器 并按 Enter 键:

![图 5.8 – ESLint 配置 – 您的代码在哪里运行?图 5.08_B18503.jpg

图 5.8 – ESLint 配置 – 你的代码在哪里运行?

  1. 对于第六个问题,你希望你的配置文件是什么格式?,选择 JSON 并按 Enter

图 5.9 – ESLint 配置 – 你希望你的配置文件是什么格式?

](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/entp-rct-dev-umijs/img/Figure_5.09_B18503.jpg)

图 5.9 – ESLint 配置 – 你希望你的配置文件是什么格式?

  1. 对于第七个问题,你想现在使用 npm 安装它们吗?,选择 No 并按 Enter,因为我们将使用 Yarn 而不是 npm:

图 5.10 – ESLint 配置 – 你想现在使用 npm 安装它们(依赖项)吗?

](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/entp-rct-dev-umijs/img/Figure_5.10_B18503.jpg)

图 5.10 – ESLint 配置 – 你想现在使用 npm 安装它们(依赖项)吗?

  1. 接下来,我们需要运行以下命令来安装所需的依赖项:

    yarn add -D eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
    
  2. 最后,让我们添加 VS Code 扩展以集成 ESLint。按 Ctrl + P,输入以下命令,然后按 Enter

    ext install dbaeumer.vscode-eslint
    

在遵循前面的步骤之后,我们的项目中应该存在一个名为 .eslintrc.json 的新文件,其中包含 ESLint 配置。让我们仔细看看这些配置:

.eslintrc.json

{
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {}
}

这些是 .eslintrc.json 文件中定义的选项:

  • env: 此选项定义全局变量。在这里,ESLint 声明我们在浏览器中工作并使用 ECMAScript 2021。

  • extends: 使用此选项,我们可以扩展其他配置文件或插件配置。在这里,ESLint 扩展了其推荐规则、react 插件规则和 typescript-eslint 插件规则。

  • parserparserOptions: 使用这些选项,我们可以定义要使用哪个代码解析器以及定义解析器选项。在这里,ESLint 使用 typescript-eslint 包将解析器设置为 TypeScript 并启用 JSX 选项。

  • plugins: 使用此选项,我们可以设置 ESLint 插件。在这里,ESLint 使用了 reacttypescript-eslint 插件。

  • rules: 使用此选项,我们可以修改 ESLint 规则以满足我们项目的需求。

我们希望 Prettier 和 EditorConfig 在代码风格上工作,而 ESLint 在代码质量上工作,因此我们需要禁用 ESLint 的格式化规则。这种方法也将防止 Prettier 和 ESLint 之间的冲突。按照以下步骤禁用 ESLint 的格式化规则:

  1. 通过运行以下命令安装禁用 ESLint 风格规则的配置:

    yarn add -D eslint-config-prettier
    
  2. 现在,通过运行以下命令安装 Prettier ESLint 插件:

    yarn add -D eslint-plugin-prettier
    
  3. 最后,按照以下方式扩展 Prettier 插件的推荐配置:

    "extends": [
      "eslint:recommended",
      "plugin:react/recommended",
      "plugin:@typescript-eslint/recommended",
      "plugin:prettier/recommended"
    ],
    

注意,我们将 Prettier 插件的配置作为 extends 数组的最后一个元素进行了扩展。对于 ESLint 正确合并共享配置,遵循此顺序很重要。

如果你打开任何页面组件,你可以在 index.tsx 文件中看到 ESLint 的作用。让我们打开位于 /src/pages/Home 文件夹中的主页组件。

图 5.11 – ESLint react-in-jsx-scope 规则

](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/entp-rct-dev-umijs/img/Figure_5.11_B18503.jpg)

图 5.11 – ESLint react-in-jsx-scope 规则

注意到 ESLint 根据我们 ESLint 配置中 react 插件的 jsx-runtime 配置找到了以下错误:

"extends": [
  "eslint:recommended",
  "plugin:react/recommended",
  "plugin:@typescript-eslint/recommended",
  "plugin:react/jsx-runtime",
  "plugin:prettier/recommended"
],

在本节中,我们安装并配置了 ESLint 以确保代码质量。我们还学习了如何通过禁用 ESLint 代码风格规则并防止这两个工具之间的冲突来将 Prettier 与 ESLint 集成。

摘要

在本章中,我们讨论了代码风格,并了解到在多个团队成员参与的专业项目中工作时,确保一致的代码风格是至关重要的。

我们学习了如何使用 EditorConfig 在 IDE 和编辑器之间定义一致的代码风格,并保持相同的格式,无论开发者的偏好如何。接下来,我们学习了如何与 Prettier 合作来强制执行代码风格,以及如何在同一项目中与 Prettier 和 EditorConfig 一起工作时避免冗余。

我们还安装并配置了 ESLint,通过分析并报告项目中的代码问题来提高代码质量。我们通过在 ESLint 配置文件中安装并扩展 Prettier 插件配置来禁用了 ESLint 风格规则。最后,我们通过扩展 ESLint React 插件中的相应配置来禁用了 react-in-jsx-scope 规则。

在下一章中,我们将讨论代码测试,并学习如何使用 JestPuppeteer 库编写测试。

第六章:测试前端应用程序

软件测试是软件开发的一个关键部分。通过实施精心设计的测试,我们可以预防错误并确保新功能不会引入错误。

在本章中,你将通过学习如何设计集成和端到端测试并将它们应用于开发过程来理解软件测试。之后,你将学习如何使用Jest(一个专注于简单性且与 React 配合良好的 JavaScript 测试框架)编写测试。你还将学习如何通过使用PuppeteerHeadless Chrome模拟用户操作来测试接口。

在本章中,我们将涵盖以下主要内容:

  • 理解软件测试

  • 使用 Jest 编写测试

  • 使用 Puppeteer 测试接口

到本章结束时,你将学会如何设计集成和端到端测试,以及如何将它们应用于提高软件质量。你将学会如何使用 Jest 编写测试,Jest 是一个用于 JavaScript 项目的编写和运行测试的工具。你还将了解如何使用 Puppeteer 和 Headless Chrome 测试接口。

技术要求

要完成本章的练习,你只需要一台安装了任何操作系统(我推荐 Ubuntu 20.04 或更高版本)的计算机,以及安装在第一章、“环境设置和 UmiJS 简介”的软件(Visual Studio Code、Node.js 和 Yarn)。

你可以在以下链接提供的 GitHub 仓库的Chapter06文件夹中找到完整的项目:github.com/PacktPublishing/Enterprise-React-Development-with-UmiJs

理解软件测试

在本节中,我们将讨论软件测试以及如何设计集成端到端测试以确保你的应用程序按预期工作。

软件测试有众多类型,我们可以将其分为两大类:功能测试,确保功能需求和规范得到满足,以及非功能测试,专注于测试系统的行为和性能。在本节中,我们将讨论两种类型的功能测试:

  • 集成测试:我们编写此类测试以确保不同的软件组件能够集成并正确工作,以提供指定的功能。

  • 端到端测试:我们编写此类测试以覆盖完整的用户流程,确保功能满足用户期望。

重要的是要提到,编写测试代码只是实施软件测试的一个任务,如果没有坚实的功能规范和测试计划,那么这并不值得。

让我们开始讨论集成测试。

理解集成测试

我们执行集成测试以确保应用中存在的不同模块能够正确工作并通信以提供所需的功能。

让我们以我们的 CRM 应用程序为例。我们通过配置 Umi 本地化插件实现了显示不同语言的应用程序的功能。我们可以执行一个集成测试来确保SelectLang组件与plugin-locale正确工作,以显示所选语言的应用程序。在这种情况下,我们需要遵循以下步骤:

  1. 将鼠标悬停在右上角的用户名上。

  2. 从下拉菜单中选择英语

  3. 检查页面是否为英语。

我们可以根据测试策略遵循测试计划手动执行集成测试。然而,更好的选择是使用自动化测试工具编写我们的测试,以便在必要时以更高的敏捷性重复测试。

我们将在接下来的章节中学习如何使用自动化测试工具来开发集成和端到端测试。接下来,让我们更深入地了解端到端测试。

理解端到端测试

如其名所示,端到端测试涵盖了从开始到结束执行任务的整个用户旅程。我们需要执行实际用户必须执行的同一步骤,验证系统的完整性和与要求的对齐。

例如,假设我们的 CRM 应用程序有一个在帖子页面打印报告的功能。为了验证这个场景,端到端测试应该包括以下步骤:

  1. 登录应用程序。

  2. 点击报告菜单。

  3. 等待图表加载并验证它们是否正确渲染。

  4. 点击打印按钮。

  5. 打开生成的 PDF 并验证报告。

如您所见,这种类型的测试涉及多个步骤,具体取决于系统的复杂性和任务的性质。我们可以根据测试计划手动执行端到端测试,或者使用自动化测试工具自动化此过程。

端到端测试通常需要强大的测试工具,并由质量保证QA)专业人员编写。然而,我们可以在开发阶段编写端到端测试。这种方法将减少 QA 阶段的问题,并加速问题的修复。

在接下来的章节中,我们将学习如何使用 Puppeteer 编写和自动化端到端测试。

软件测试是一个广泛的主题。如果您想了解更多关于这个主题的信息,我推荐阅读www.ibm.com/topics/software-testing上的文章。

在本节中,我们通过学习如何设计集成和端到端测试来讨论软件测试。接下来,您将学习如何在 JavaScript 项目中使用 Jest 编写测试。

使用 Jest 编写测试

在本节中,您将学习如何在 JavaScript 项目中使用Jest 框架编写测试。

Jest 是一个针对 JavaScript 项目的快速且可靠的测试框架,注重简洁性。它与 Babel、TypeScript、Node、React、Angular、Vue 和其他工具兼容。

安装后,我们可以开始使用 Jest 而无需任何额外配置。在我们的例子中,我们甚至不需要安装 Jest,因为 Umi 已经通过 umi-test 包提供了 Jest。

考虑以下使用 Jest 编写的端到端测试,以测试登录流程:

it('[END_TO_END] Should sucessfully login', async () => {
  const page = await context.newPage();
  await page.goto('http://localhost:8000');
  await page.waitForNavigation();
  await page.type('#username', 'john@doe.com');
  await page.type('#password', 'user');
  await page.click('#loginbtn');
  const loggedUser = await page.waitForSelector('#loggeduser');
  expect(loggedUser).toBeTruthy();
});

在这个测试中,所有指令都写在 it 方法内部。如果您愿意,也可以使用 test 方法。这两种方法之间的区别只是语义上的。

在这里,it 方法接收两个参数:第一个参数是测试名称,第二个是一个执行测试指令的异步函数。

注意查看页面上的 expect 方法与 toBeTruthy loggeduser ID 的组合。

我们使用匹配器来测试值与不同条件的一致性。您可以在 jestjs.io/docs/expect 找到可用的所有 Jest 匹配器的完整列表。

接下来,您将看到如何通过创建测试套件来组织相关测试。

理解 describe 方法

当编写多个相关测试时,您应该使用 describe 方法在测试套件中组织它们,如下例所示:

describe('Math test suite', () => {
  it('should return 2', () => {
    const value = 1 + 1;
    expect(value).toBe(2);
  });
  it('should return 25', () => {
    const value = 5 * 5;
    expect(value).toBe(25);
  });
});

在这个例子中,我们使用了 describe 方法为与数学问题相关的两个测试创建了一个组。

让我们看看我们如何在整个测试套件或每个测试运行之前和之后执行一些设置工作。

在测试前后执行指令

有时,在运行测试之前,您可能需要进行一些设置,例如初始化数据库连接或生成模拟数据。您可以通过在 beforeAll 方法中定义指令来在所有测试运行之前执行,或者通过在 beforeEach 方法中定义指令来在每个测试运行之前执行。考虑以下示例:

describe('Product test suite', () => {
  let connection: DBConnection;
  let product: Product;
  beforeAll(async () => {
    connection = await database.connect();
  });
  beforeEach(async () => {
    product = connection.query(query);
  });
  it('should be greater than 200', async () => {
    expect(product.units).toBeGreaterThan(200);
  });
  it('should be true', async () => {
    expect(product.active).toBeTruthy();
  });
});

在这个例子中,在所有测试运行之前,我们打开了数据库连接,在每个测试运行之前,我们使用该连接查询数据库中的产品。

此外,在这个例子中,我们需要在运行测试套件后关闭数据库连接。我们可以通过添加 afterAll 方法来实现,如下一个示例所示:

afterAll(() => connection.close());

afterAll 方法类似,您可以使用 afterEach 方法在每个测试运行后执行指令。

在本节中,您学习了如何使用 Jest 框架编写测试。您学习了如何创建测试套件以及在测试运行前后执行指令。

接下来,让我们了解 Puppeteer 并为我们的应用程序编写集成和端到端测试。

使用 Puppeteer 测试接口

在本节中,您将学习如何使用 Puppeteer无头 Chrome 浏览器 编写集成和端到端测试。

Puppeteer 是一个 Node 库,它通过 DevTools 协议(或 Firefox 的远程协议)控制 Chrome、Chromium 或 Firefox 浏览器,这使得它成为在测试期间模拟真实场景的绝佳工具。

当我们启动一个新的浏览器实例时,Puppeteer 将默认使用 Chrome 的无头模式。Chrome 的无头模式仅包括浏览器引擎,没有用户界面。Puppeteer 使用 Chrome DevTools 协议来控制浏览器。

使用 Puppeteer,我们可以对页面进行截图,通过模拟多种移动设备(如平板电脑和智能手机)来测试响应性,等等。

你可以在 developers.google.com/web/tools/puppeteer 的文档页面了解更多关于 Puppeteer 的信息。

我们将编写一个集成测试和一个端到端测试来展示 Puppeteer 和 Jest 的使用。

测试访问和布局插件

让我们从运行以下命令来安装 Puppeteer 开始:

yarn add -D puppeteer

Puppeteer 的配置与 Jest 一样简单。通过运行此命令,Puppeteer 将安装最新版本的 Chromium 浏览器,然后我们可以开始使用它。

现在,按照以下步骤创建集成测试:

  1. 在项目的根目录下创建一个名为 tests 的新文件夹。

  2. tests 文件夹中,创建一个名为 integration.test.ts 的新文件。Jest 将执行所有名称包含 .test.ts 的文件。

  3. integration.test.ts 文件中,创建一个测试套件,如下所示:

    import puppeteer, { Browser, BrowserContext, Page } from 'puppeteer';
    describe('[SUITE] Integration testing', () => {
      let context: BrowserContext;
      let browser: Browser;
      beforeAll(async () => {
        browser = await puppeteer.launch();
      });
      beforeEach(async () => {
        context = 
          await browser.createIncognitoBrowserContext();
      });
      afterEach(() => context.close());
      afterAll(() => browser.close());
    });
    

在这里,在所有测试运行之前,我们启动 Puppeteer 并将实例存储在 browser 变量中。默认情况下,Puppeteer 将以无头模式启动 Chromium。不过,你可以通过设置 headless 选项来启动完整浏览器版本,如下所示:

browser = await puppeteer.launch({ headless: false });

通过将 headless 选项设置为 false,你可以看到 Puppeteer 打开窗口并执行测试。

图 6.1 – 在完整浏览器版本中运行集成测试

图 6.1 – 在完整浏览器版本中运行集成测试

在每次测试运行之前,我们通过打开一个隐身窗口来创建一个匿名浏览器上下文,以在隔离环境中执行它们,并将隐身窗口存储在 context 变量中。在每次测试运行之后,我们关闭隐身窗口,在整个测试套件运行之后,我们关闭浏览器。

  1. 你会注意到 TypeScript 找不到 Jest 类型。我们必须通过运行以下命令来安装声明文件:

    yarn add -D @types/jest
    

现在,让我们创建一个集成测试来确保 Umi 访问插件能够正确地与布局插件协同工作,在未经授权的用户尝试访问受限制的页面时显示 403 错误页面。

  1. 将以下测试添加到 tests 文件夹下的 integration.test.ts 文件中的测试套件:

    it('[INTEGRATION] Should successfully block unauthorized access (plugin-access)', async () => {
      const page = await context.newPage();
      page.setDefaultTimeout(10000);
      await login(page);
      await page.goto('http://localhost:8000/workflow');
      const message = 
        await page.waitForSelector('#unauthorized');
      const value = await page.evaluate((el) => 
        el.textContent, message);
      expect(value).toBe('Sorry, you are not authorized to 
      access this page.');
    });
    

在这个测试中,在打开页面并将 async 操作的默认超时设置为 10000 毫秒之后,Puppeteer 执行以下步骤:

  • 以 John Doe 的身份登录应用程序

  • 跳转到工作流程页面

  • 选择具有 unauthorized ID 的元素

  • 评估元素内的文本消息并测试消息是否正确

注意,我们使用了 waitForSelector 方法来确保在选中元素时它已经被渲染。

  1. 接下来,我们需要创建 login 函数。在 describe 方法之前添加 login 函数,如下所示:

    async function login(page: Page) {
      await page.goto('http://localhost:8000');
      await page.waitForNavigation();
      await page.type('#username', 'john@doe.com');
      await page.type('#password', 'user');
      await page.click('#loginbtn');
    }
    

此函数将执行以约翰·多伊(John Doe)身份登录应用程序的步骤。我们可以在测试套件中的其他测试中重用 login 函数。注意我们使用了 waitForNavigation 方法来确保在执行步骤之前组件已渲染。

现在,我们需要将 unauthorized ID 添加到包含我们将验证的文本的元素中。

  1. 打开位于 src 文件夹中的 app.tsx 文件,并修改布局 unAccessible 配置选项,如下所示:

    unAccessible: (
      <Result
        status="403"
        title="403"
        subTitle={
          <span id="unauthorized">
            Sorry, you are not authorized to access this 
            page.
          </span>
        }
        extra={
          <Button type="primary" onClick={() => 
            history.push('/')}>
            Back to Home
          </Button>
        }
      />
    ),
    

我们将包含所需 id 属性的文本包裹在一个 span 标签中。

我们还需要将 id 属性添加到登录表单输入中。

  1. 打开位于 src/pages/Login/LoginForm 下的 index.tsx 文件,并通过添加 username ID 来修改用户名输入,如下所示:

    <Form.Item
      name="username"
      rules={[
        {
          required: true,
          message: formatMessage({ id: 'login.alert.username' }),
        },
      ]}
    >
      <Input
        id="username"
        prefix={<UserOutlined className="site-form-item-icon" 
          />}
        placeholder={formatMessage({ id: 'login.placeholder.
          username' })}
      />
    </Form.Item>
    
  2. 接下来,通过添加 password ID 来修改密码输入,如下所示:

    <Form.Item
      name="password"
      rules={[
        {
          required: true,
          message: formatMessage({ id: 'login.alert.password' 
            }),
        },
      ]}
    >
      <Input
        id="password"
        prefix={<LockOutlined className="site-form-item-icon" 
          />}
        type="password"
        placeholder={formatMessage({ 
          id: 'login.placeholder.password' })}
      />
    </Form.Item>
    
  3. 最后,将 loginbtn ID 添加到登录按钮中,如下所示:

    <Form.Item>
      <Button
        id="loginbtn"
        type="primary"
        htmlType="submit"
        className="login-form-button"
      >
        <FormattedMessage id="login.form.login" />
      </Button>
      <FormattedMessage id="login.form.or" />{' '}
      <a href="">
        <FormattedMessage id="login.form.register" />!
      </a>
    </Form.Item>
    

您可以通过运行 yarn test 命令来执行测试。结果应如下截图所示:

![Figure 6.2 – Integration test result]

![img/Figure_6.02_B18503.jpg]

图 6.2 – 集成测试结果

现在,让我们创建一个端到端测试来确保编辑机会的功能按预期工作。

测试机会编辑功能

按照以下步骤创建端到端测试,以确保在机会页面上编辑功能按预期工作:

  1. 我们将在 tests 文件夹中创建一个名为 end2end.test.ts 的单独文件,并创建 describe 方法,如下所示:

    import puppeteer, { Browser, BrowserContext, Page } from 'puppeteer';
    describe('[SUITE] End-to-end testing', () => {
      let context: BrowserContext;
      let browser: Browser;
      beforeAll(async () => {
        browser = await puppeteer.launch();
      });
      beforeEach(async () => {
        context = 
          await browser.createIncognitoBrowserContext();
      });
      afterEach(() => context.close());
      afterAll(() => browser.close());
    });
    
  2. 接下来,将端到端测试添加到 describe 方法中,如下所示:

    it('[END_TO_END] Should sucessfully edit opportunity', async () => {
      const page = await context.newPage();
      page.setDefaultTimeout(10000);
      await page.goto('http://localhost:8000');
      await page.waitForNavigation();
    });
    

我们已经为 Puppeteer 编写了打开新页面并设置默认超时为 1000 的说明,转到登录页面,并等待页面加载。

  1. 添加登录应用程序的说明,如下所示:

    await page.type('#username', 'john@doe.com');
    await page.type('#password', 'user');
    await page.click('#loginbtn');
    await page.goto('http://localhost:8000/opportunities');
    

Puppeteer 将在具有 username ID 的输入中输入用户的电子邮件地址,并在具有 password ID 的输入中输入密码,然后点击具有 loginbtn ID 的按钮。最后,Puppeteer 将导航到机会页面。

  1. 添加编辑机会主题的步骤,如下所示:

    await(await page.waitForSelector('#editopportunity')).click();
    const topicInput = await page.$(
    'table > tbody > tr > td > div > div > div > div > span 
      > input',
    );
    await topicInput.click({ clickCount: 3 });
    await topicInput.type('Opportunity topic');
    await(await page.waitForSelector('#save')).click();
    

Puppeteer 将等待具有 id 等于 editopportunity 的元素渲染,然后点击它并通过三击输入元素选择文本。接下来,Puppeteer 将在主题输入中输入新文本并保存机会。

  1. 现在,让我们添加评估结果的步骤,如下所示:

    const topicCell = await page.waitForSelector(
      'tr[data-row-key="0"] > .ant-table-cell',
    );
    const value = await page.evaluate((el) => el.textContent, 
      topicCell);
    expect(value).toBe('Opportunity topic');
    

Puppeteer 将选择机会表的第一行的主题单元格并评估其文本内容。接下来,Jest 将测试该值以确保其正确性。

  1. 最后,让我们将 id 属性添加到测试期间需要查找的元素中。在 columns.tsx 文件中,在 pages/Opportunities 文件夹下,将 id 属性添加到编辑选项锚点中,如下所示:

    {
      title: <FormattedMessage id="table.options" />,
      valueType: 'option',
      hideInSetting: true,
      hideInDescriptions: true,
      render: (_, record, __, action) => [
        <a
          key="editable"
          id="editopportunity"
          onClick={() => {
          action?.startEditable(record.id as number);
        }}
        >
        <FormattedMessage id="table.edit" />
      </a>,
      <a key="more" onClick={() => history.push(`/
        opportunity/${record.id}`)}>
        <FormattedMessage id="table.more" />
      </a>,
      ],
    },
    
  2. 在同一文件夹中的index.ts文件中,将saveText属性添加到ProTable组件的可编辑属性中,如下所示:

    editable={{
      type: 'multiple',
      deletePopconfirmMessage: <FormattedMessage 
        id="table.confirm" />,
      saveText: <span id="save">save</span>,
      deleteText: <FormattedMessage id="table.disable" />,
      onDelete: async (key) => disable(key as string),
      onSave: async (_, record) => update(record),
    }}
    

在执行测试之前,让我们将--runInBand标志添加到package.json文件中的umi-test命令中,如下所示:

"test": "umi-test --runInBand",

此标志将防止这两个测试之间的竞争条件,因为我们正在使用模拟 API 来模拟后端。

现在,你可以通过运行yarn test命令来执行测试。结果应该如下所示:

![Figure 6.3 – End-to-end test resultFigure 6.03 – End-to-end test result

Figure 6.3 – End-to-end test result

在本节中,你学习了如何使用 Puppeteer 编写集成测试和端到端测试。为了演示如何使用 Puppeteer 与 Jest 结合,我们创建了一个集成测试来确保 Umi 本地化插件与布局插件正确协同工作以渲染 403 错误页面。我们还创建了一个端到端测试来确保编辑机会的功能按预期工作。

摘要

在本章中,我们通过学习如何设计集成和端到端测试来讨论软件测试。你学习了如何在 React 项目中使用 Jest 框架编写测试。你看到了如何使用describetest(或it)方法来编写和组织相关测试。你还学习了如何在测试运行前后使用beforeAllbeforeEachafterAllafterEach方法执行指令。

然后,你学习了如何通过模拟用户界面上的用户交互来使用 Puppeteer 和 Headless Chrome 编写测试。为了演示如何使用 Puppeteer 与 Jest 结合,我们创建了一个集成测试来确保 Umi 本地化插件与布局插件正确协同工作,并创建了一个端到端测试来确保编辑机会的功能按预期工作。

在下一章中,我们将学习如何编译和部署我们的应用程序到在线服务。

第七章:单页应用部署

在上一章中,我们讨论了软件测试以及如何在开发过程中编写测试并应用它来防止错误并提高软件质量。

软件开发生命周期的最后一步是将应用程序部署到在线服务。在本章中,我们将使用开源的 Mockachino 服务创建一个简单的模拟服务器作为你的应用程序后端。你将学习如何构建应用程序以及由 Umi 生成的编译源代码文件。你还将学习如何在 AWS Amplify 上部署和配置你的应用程序。

本章,我们将涵盖以下主要主题:

  • 使用 Mockachino 创建模拟服务器

  • 编译应用程序和设置环境变量

  • 在 AWS Amplify 上托管应用

到本章结束时,你将学会如何构建应用程序以及由 Umi 生成的编译源代码文件。你还将了解如何快速使用 Mockachino 服务创建模拟服务器。你还将学会如何在 AWS Amplify 上部署和配置单页应用程序。

技术要求

要完成本章的练习,你只需要一台安装了任何操作系统(我推荐 Ubuntu 20.04 或更高版本)的电脑以及 第一章**,环境设置和 UmiJS 简介(VS Code、Node.js 和 Yarn)中安装的软件。

你可以在 GitHub 仓库中找到完整的项目,该仓库位于 github.com/PacktPublishing/Enterprise-React-Development-with-UmiJsChapter07 文件夹中。

使用 Mockachino 创建模拟服务器

在本节中,我们将使用 Mockachino 创建模拟服务器来模拟应用程序的后端服务。

我们的应用程序只是 CRM 系统的表现层,用户可以可视化并输入数据。在部署之前,我们需要在线后端服务,我们的应用程序可以连接以处理、存储和接收数据。

后端服务是由后端开发者实现的 API 和微服务,用于安全高效地应用业务逻辑并存储诸如机会、活动、客户和用户信息等信息。

由于本书的目的是用 UmiJS 教授 React 开发,我们不会构建后端服务。我们将使用 Mockachino 来模拟后端。

Mockachino 是一个创建模拟服务器的简单服务。我们只需要定义一个端点,Mockachino 就会提供空间和秘密链接,以便在需要时访问该空间。

让我们从创建获取用户信息的路由开始。导航到 www.mockachino.com/ 并按照以下步骤操作:

  1. api/currentUser

  2. 接下来,在 HTTP 响应体 字段中,输入以下 JSON 响应:

    {
      "company": "Umi Group",
      "name": "Marry Doe",
      "role": {
        "id": 0,
        "title": "Administrator"
      },
      "isLoggedIn": "true",
      "id": "1"
    }
    
  3. 点击创建,Mockachino 将提供一个秘密链接,如图下所示:

![图 7.1 – Mockachino 空间秘密链接图片

图 7.1 – Mockachino 空间秘密链接

通过点击端点路由(GET /api/currentUser),您可以编辑端点属性,如路径、HTTP 响应头和响应体。

要创建一个新的路由,请点击mockachino.md文件。

为了您的方便,我在本书 GitHub 仓库的Chapter07文件夹中创建了一个名为mockachino.md的 markdown 文件。在这个文件中,您将找到在接下来的章节中必须创建的所有路由和响应。

在本节中,我们使用 Mockachino 创建了一个模拟服务器来模拟后端服务。接下来,让我们学习如何打包应用程序并设置环境变量。

编译应用程序和设置环境变量

在本节中,您将了解 Umi 将生成哪些文件以及如何编译应用程序。我们还将设置一个环境变量来配置发送 HTTP 请求的 URL。

我们需要将我们的组件和依赖项转换和编译成网络浏览器可以解释和渲染的格式,然后再部署应用程序。

运行配置在我们的 package scripts 中的yarn build命令。此命令将编译应用程序并将编译后的源代码文件放置在dist文件夹中。

![图 7.2 – 编译后的源代码文件图片

图 7.2 – 编译后的源代码文件

您将在dist文件夹中找到三个文件:

  • index.html:这是包含我们应用程序入口点的 HTML 文档。

  • umi.css:这是包含项目中所有由 LESS 文件生成的应用程序样式的压缩样式表。

  • umi.js:这是包含执行我们应用程序所需的所有 JavaScript 代码的压缩文件。

现在,我们需要在互联网上的静态服务器上托管这些文件。当用户导航到服务器的公共地址时,浏览器将请求并接收index.html文档,这是我们应用程序的入口点。我们将在下一节中将应用程序托管在 Amplify 上。

现在,让我们调整您的应用程序以向 Mockachino 发送请求。

配置 API URL 环境变量

如前所述,在生产环境中,我们没有与我们的应用程序一起运行的模拟服务器。我们将向 Mockachino 发送 HTTP 请求,因此我们需要更改services文件夹中所有函数的 URL 参数。我们将通过配置环境变量来完成此操作。

Umi 可以在构建过程中读取环境变量,并在我们的应用程序中使用它们的值;我们只需要设置 Umi 的define配置选项。

按照以下步骤创建一个环境变量来设置 API URL:

  1. 在项目的根目录下创建一个名为.env的新文件,并创建一个名为API_URL的变量,如下所示:

    API_URL=https://www.mockachino.com/secret
    

将值替换为 Mockachino 提供的 URL。

  1. define选项添加到config.ts文件中的配置,如下所示:

    define: {
       API_URL: process.env.API_HOST,
    },
    

此配置在项目中定义了API_URL变量。

  1. 现在,让我们创建一个文件来导出变量并防止 TypeScript 警告。在config文件夹中创建一个名为env.ts的新文件,并按照以下方式导出变量:

    // @ts-nocheck
    export default {
      API_URL: API_URL,
    };
    
  2. src/services文件夹中的user.ts文件中,按照以下方式导入env.ts文件:

    import env from '../../config/env';
    
  3. 接下来,将API_URL添加到request函数的第一个参数中,如下所示:

    return request<User>(`${env.API_URL}/api/currentUser`, {
      method: 'GET',
      headers: { 'Content-Type': 'application/json' },
      params: { context: contextId },
    });
    

按照最后两个步骤更改services文件夹中所有文件的request函数。

在本节中,您学习了如何编译应用程序的源代码文件以及 Umi 在构建过程中生成的文件。我们还创建了一个环境变量,并将请求更改为使用 Mockachino 作为后端。

现在,我们将使用 Amplify 控制台服务在 AWS 上托管我们的应用程序。

在 AWS Amplify 上托管应用程序

在本节中,您将学习如何通过使用 Amplify 控制台托管我们的应用程序来在亚马逊网络服务AWS)上部署和配置单页应用程序。

AWS Amplify是一套灵活的工具,供网络和移动前端开发者使用,通过 AWS 的各种服务创建和部署应用程序。使用 Amplify,你可以快速构建和部署全栈应用程序,无需研究和学习单个 AWS 服务。

我们将仅使用 Amplify 来托管我们的应用程序,但您可以使用 Amplify 框架和 Amplify Studio 创建后端服务,添加身份验证、人工智能、机器学习等。如果您想了解更多信息,请访问框架的文档页面docs.amplify.aws/

在进行以下步骤之前,您需要将项目推送到您个人 GitHub 账户的新仓库中。

此外,您还需要创建一个免费的 AWS 账户。访问aws.amazon.com/free,点击创建免费账户,填写所需信息以创建您的账户。

现在,在将代码推送到新仓库并创建您的 AWS 账户后,按照以下步骤在 Amplify 上托管我们的应用程序:

  1. 导航到console.aws.amazon.com/amplify/home并登录您的 AWS 账户。

  2. 点击以下屏幕截图中的高亮菜单,然后点击所有应用

图 7.3 – 左侧菜单

图 7.3 – 左侧菜单

  1. 在页面右上角,点击新建应用下拉菜单并选择托管网络应用

图 7.4 – 托管网络应用选项

图 7.4 – 托管网络应用选项

  1. 现在,在从现有代码部分选择GitHub,点击继续,并登录您的 GitHub 账户:

图 7.5 – 选择源 Git 提供者

图 7.5 – 选择源 Git 提供者

  1. 接下来,选择您为我们项目创建的仓库,然后点击 下一步

![Figure_7.6 – 选择 GitHub 仓库

![Figure_7.06_B18503.jpg]

图 7.6 – 选择 GitHub 仓库

  1. 步骤 2 配置构建设置 中,在 构建和测试设置 部分中,点击 编辑,将第 12 行修改如下,然后点击 保存

    baseDirectory: /dist
    

此配置将设置 Amplify 在运行自动化管道时查找源代码的位置。

![Figure_7.7 – 配置源代码基本目录

![Figure_7.07_B18503.jpg]

图 7.7 – 配置源代码基本目录

  1. 点击 API_URL,然后在 字段中粘贴 Mockachino 的密钥链接:

![Figure_7.8 – 创建环境变量

![Figure_7.08_B18503.jpg]

图 7.8 – 创建环境变量

  1. 现在,点击 下一步,在下一步中,审查配置并点击 保存和部署

![Figure_7.9 – 审查和部署应用程序

![Figure_7.09_B18503.jpg]

图 7.9 – 审查和部署应用程序

  1. 等待管道成功,然后点击公共地址以访问应用程序:

![Figure_7.10 – 应用程序公共地址

![Figure_7.10_B18503.jpg]

图 7.10 – 应用程序公共地址

现在,让我们更仔细地看看更多的 Amplify 设置。

了解更多 Amplify 设置

当托管单页应用程序时,有必要配置服务器以仅对带有 index.html 页面的请求做出响应;否则,服务器将返回错误,因为其他页面不存在于服务器上。

Amplify 在 index.html 文件中提供了一个默认的路由规则。

![Figure_7.11 – 重写和重定向配置

![Figure_7.11_B18503.jpg]

图 7.11 – 重写和重定向配置

Amplify 还在 amplifyapp 域上提供公共地址,但您可以通过在左侧菜单中访问 域名管理 来轻松添加自定义域名。

域名可以是 AWS Route 53 或其他提供商的托管区域,AWS 还提供免费的 SSL 证书来保护您应用程序的域名。

Figure_7.12 – Amplify 域名管理

图 7.12 – Amplify 域名管理

在本节中,您创建了一个免费的 AWS 账户,并通过将 Amplify 与您 GitHub 账户中的仓库连接来在 AWS 上托管您的应用程序。您还学习了如何在 Amplify 控制台中配置重写和重定向以及管理您的自定义域名。

摘要

在本章中,我们使用 Mockachino(一个用于快速模拟服务器的开源项目)为我们的应用程序创建了一个模拟服务器。您还学习了 Umi 在构建过程中为浏览器解释和渲染应用程序而生成的文件。您创建了一个环境变量来定义我们的应用程序将用于发送请求的 URL。

你已经学会了如何将你的应用程序推送到个人 GitHub 账户的仓库中,并创建了一个免费的 AWS 账户。接下来,你通过将 AWS Amplify 连接到你的 GitHub 仓库,在 AWS 上托管了你的应用程序。你还学会了如何配置重写和重定向,并在 Amplify 控制台上管理你的自定义域名。

我希望这本书已经帮助你学会了如何使用 UmiJS 结合 Ant Design 来创建强大且专业的 React 应用程序,这些应用程序提供了极佳的用户体验。继续练习和探索这本书中学到的技术。

Packt.com

订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助你规划个人发展并推进职业生涯。更多信息,请访问我们的网站。

第八章:为什么订阅?

  • 使用来自超过 4,000 位行业专业人士的实用电子书和视频,节省学习时间,多花时间编码

  • 通过为你量身定制的 Skill Plans 提高学习效果

  • 每月免费获得一本电子书或视频

  • 完全可搜索,便于快速访问关键信息

  • 复制粘贴、打印和收藏内容

你知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?你可以在packt.com升级到电子书版本,作为印刷书客户,你有权获得电子书副本的折扣。如需了解更多详情,请联系我们customercare@packtpub.com

www.packt.com,你还可以阅读一系列免费的技术文章,注册各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。

你可能还会喜欢的其他书籍

如果你喜欢这本书,你可能对 Packt 的其他书籍也感兴趣:

使用 GraphQL 和 React 的全栈 Web 开发 – 第二版

西班牙·格雷贝

ISBN: 978-1-80107-788-0

  • 通过实现模型和模式使用 Apollo 和 Sequelize 构建 GraphQL API

  • 设置 Apollo 客户端并使用 React 构建前端组件

  • 编写可重用的 React 组件并使用 React Hooks

  • 使用 GraphQL 验证和查询用户数据

  • 使用 Mocha 为你的全栈应用程序编写测试用例

  • 使用 Docker 和 CircleCI 将你的应用程序部署到 AWS

使用 React Hooks 的微状态管理

大岛耕作

ISBN: 978-1-80181-237-5

  • 理解微状态管理以及如何处理全局状态

  • 使用微状态管理和 React Hooks 构建库

  • 发现使用 React Hooks 的微方法是如何变得容易的

  • 理解组件状态和模块状态之间的区别

  • 探索实现全局状态的几种方法

  • 通过具体的示例和库如 Zustand、Jotai 和 Valtio 熟练掌握

Packt 正在寻找像你这样的作者

如果你对成为 Packt 的作者感兴趣,请访问authors.packtpub.com并今天申请。我们已与成千上万的开发者和技术专业人士合作,就像你一样,帮助他们将见解分享给全球技术社区。你可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。

分享你的想法

你好,

我是 Douglas Alves Venancio,著有《使用 UmiJS 进行企业级 React 开发》。我真心希望您喜欢阅读这本书,并觉得它对提高您在 UmiJS 中的生产力和效率有所帮助。

如果您能在 Amazon 上留下对《使用 UmiJS 进行企业级 React 开发》的评价,分享您的想法,这将对我(以及其他潜在读者!)非常有帮助。

前往以下链接留下您的评价:

packt.link/r/1803238968

您的评价将帮助我了解这本书哪些地方做得好,以及未来版本可以改进的地方,所以您的评价真的非常宝贵。

祝好,

Douglas Alves Venancio

posted @ 2025-09-08 13:03  绝不原创的飞龙  阅读(8)  评论(0)    收藏  举报