在使用Next 4年后,我如何接近和构建企业前端应用程序。
#javascript #react #typescript #nextjs

介绍

在快速的前端开发世界中,保持曲线的领先地位对于建立成功的企业应用程序至关重要。在使用Next.js的四年之后,包括尾风CSS,TypeScript,TurborePo,Eslint,React Query等功能强大的工具包,我获得了宝贵的见解和最佳实践,可以与其他开发人员分享。在此博客文章中,我们将探讨如何架构和构建大型企业的前端应用程序,最大化性能,可维护性和可伸缩性。

nb 本文表示个人观点,我提倡的方法可能不适合您的特定情况。

有效企业前端体系结构的指导原则

在为企业规模应用程序的架构架构解决方案方面时,拥有明确定义的原则可能是北极星,可以使您的发展努力保持课程。在本节中,我将分享从企业环境中Next.js的经验中出现的指导原则。

模块化和组件

原理:划分和征服

在企业应用程序的庞大景观中,代码可以很快成为不守规矩的野兽。拥抱模块化和组件,将您的前端分解为可管理的部分。将组件视为乐高块,每个组件都有特定目的。这不仅可以增强代码可重用性,而且还简化了开发团队内的维护和协作。不仅考虑将应用程序分割为较小的组件,还可以将其分解为较小的独立应用程序。这是诸如Turbo Repo Excel之类的工具。

关注(SOC)的分离

原理:保持代码库整洁

保持代码理智,遵守关注(SOC)原则的分离。确保您的组件专注于各自的职责,无论是渲染UI,处理业务逻辑还是管理状态。这种隔离不仅使代码更易于理解,而且还促进了测试和调试。

通过设计可扩展性

原理:增长计划

企业应用程序不是静态的;他们进化。考虑可扩展性,设计前端体系结构。这意味着选择可以适应流量增加,数据量和功能复杂性的模式和工具。 Next.js的可扩展性设计可能是这项工作中有价值的盟友。

可维护性和代码质量

原理:护理工艺

代码是您产品的基础。优先考虑第一天的可维护性和代码质量。执行编码标准,进行代码审查并投资自动测试。维护良好的代码库不仅更容易使用,而且更容易容易出现错误和回归。在我的工作中,我最近开发了一个component library and a basic style guide,以在我们的前端应用程序上执行标准。不要介意他们尚未完成的文档。

默认情况下可访问性

原理:从起点开始

可访问性是现代网络开发的不可谈判的方面。从一开始就将其作为默认做法。无论残疾如何,确保您的应用都可以使用。利用Next.js支持可访问性标准和工具来创建包容性用户体验。我使用Radix UI之类的工具来用于某些需要可访问性的组件,例如标签,下拉列表等。

面向性能的发展

原理:速度很重要

企业用户期望活泼的体验。优先考虑绩效。优化资产,最小化不必要的请求,并利用Next.js的性能功能,例如自动代码分裂,悬疑和图像优化。快速应用程序不仅使用户取悦用户,而且会对SEO产生积极影响。

首先安全

原理:守卫你的城堡

安全性应编织成前端体系结构的结构。预防常见漏洞,例如跨站点脚本(XSS)和跨站点申请伪造(CSRF)。对安全更新和最佳实践保持警惕,并考虑Next.js的内置安全功能作为额外的防御层。

国际化(I18N)和本地化(L10N)

原理:全球思考

在我们相互联系的世界中,全球认为是必不可少的。从一开始就实施国际化(I18N)和本地化(L10N),以迎合多样化的用户群。 Next.js为这些功能提供了出色的支持,使创建多语言应用程序变得更加容易。

与Next.js合作时,这些指导原则构成了有效的企业前端体系结构的基岩。它们充当指南针,确保您的开发工作与大规模应用程序的需求保持一致,从而使它们稳健,可维护和用户友好。在以下各节中,我们将更深入地研究如何将这些原则转化为可行的策略和最佳实践。

文件夹和文件结构

在React中,通过经过深思熟虑的文件夹结构组织项目对于可维护性和可扩展性至关重要。一种常见的方法是根据文件的功能和目的来安排文件。这是我通常用于应用程序的示例文件夹结构:

├─ src/
│ ├─ components/
│ │ ├─ ui/
│ │ │ ├─ Button/
│ │ │ ├─ Input/
│ │ │ ├─ ...
│ │ │ └─ index.tsx
│ │ ├─ shared/
│ │ │ ├─ Navbar/
│ │ └─ charts/
│ │ │ ├─ Bar/
│ ├─ modules/
│ │ ├─ HomePage/
│ │ ├─ ProductAddPage/
│ │ ├─ ProductPage/
│ │ ├─ ProductsPage/
│ │ │ ├─ api/
│ │ │ │ └─ useGetProducts/
│ │ │ ├─ components/
│ │ │ │ ├─ ProductItem/
│ │ │ │ ├─ ProductsStatistics/
│ │ │ │ └─ ...
│ │ │ ├─ utils/
│ │ │ │ └─ filterProductsByType/
│ │ │ └─ index.tsx
│ │ ├─ hooks/
│ │ ├─ consts/
│ │ └─ types/
│ │ └─ lib/
| | └─ styles/
│ │ │ ├─ global.css
│ │ └─ ...
│ ├─ public/
│ │ ├─ ...
│ │ └─ index.tsx
│ ├─ eslintrc.js
│ ├─ package.json
│ └─ tsconfig.json
└─ ...
  • src/组件:此目录包含您的UI组件。它进一步细分为ui,以获取通用UI组件的ui,而shared的组件可能会在您应用程序的不同部分中重复使用。

  • src/模块:此目录包含您应用程序的不同视图或页面。每个模块可能具有自己的文件夹,其中包含用于API调用,组件和实用程序功能的子目录。

  • src/pages :如果您使用的是next.js,则只能将此文件夹用作应用程序的入口点。这里没有业务逻辑。页面文件夹中的组件只能从模块文件夹呈现页面。

  • src/模块/productsPage :此模块与产品相关,并且包含用于API调用的子目录,组件(例如ProductItemProductsStatistics)和实用程序功能(filterProductsByType)。 /p>

  • src/lib :该文件夹可能包含实用程序功能,以后可以将其转换为应用程序多个部分中使用的软件包。它与 src/utils 不同,后者可能包含实用程序功能,这些功能以后转换为软件包没有意义。

  • src/styles :此目录保存全局样式(global.css)和其他与样式相关的文件。

  • src/public :此文件夹包含不经过构建过程的静态资产。它可能包括图像,字体和index.html文件。

  • src/consts src/types :这些目录可能分别包含常数和打字稿类型定义。

  • src/hooks :此目录可以容纳整个应用程序中使用的自定义挂钩。

  • eslintrc.js :这是一个流行的JavaScript刺激工具Eslint的配置文件。它用于执行编码约定并在您的代码中捕获潜在的错误。

配置了tsconfig文件,以便例如,如果您想导入Button组件,则可以像这样做的import { Button } from '@/components/ui'。以下是如何配置tsconfig.json的片段。

{
  ...
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

打字稿编码约定

我遵循的惯例是受本指南here.的启发,我强烈建议您阅读它,下面的代码片段来自该指南。

所有类型都必须使用类型别名定义

// ❌ Avoid interface definitions unless you need to extend or implement them
interface UserRole = 'admin' | 'guest'; // invalid - interface can't define (commonly used) type unions

interface UserInfo {
  name: string;
  role: 'admin' | 'guest';
}

// ✅ Use type definition
type UserRole = 'admin' | 'guest';

type UserInfo = {
  name: string;
  role: UserRole;
};

避免使用多个参数

// ❌ Avoid having multiple arguments
transformUserInput('client', false, 60, 120, null, true, 2000);

// ✅ Use options object as argument
transformUserInput({
  method: 'client',
  isValidated: false,
  minLines: 60,
  maxLines: 120,
  defaultInput: null,
  shouldLog: true,
  timeout: 2000,
});

命名约定

尽管确定最佳名称可能具有挑战性,但要通过遵守已建立的约定来增强代码的可读性并保持未来开发人员的一致性:

变量

  • 当地人 骆驼香烟盒 咀嚼11,咀嚼11
  • 布尔值 前缀为ishas等。 Chills15,咀嚼16
  • 常数 大写 PRODUCT_ID
  • 对象常数

单数,用const主张大写,可选地满足类型(如果有的话)。

  const ORDER_STATUS = {
    pending: 'pending',
    fulfilled: 'fulfilled',
    error: 'error',
  } as const satisfies OrderStatus;

功能

骆驼盒

filterProductsByTypeformatCurrency

仿制药

名称以大写字母t TRequestTFooBar开头(类似于.Net internal实施)。


避免使用一个字符TK等的(流行惯例)命名仿制药,我们介绍的变量越多,误解它们的越容易。

// ❌ Avoid naming generics with one character
const createPair = <T, K extends string>(first: T, second: K): [T, K] => {
  return [first, second];
};
const pair = createPair(1, 'a');

// ✅ Name starts with the capital letter T
const createPair = <TFirst, TSecond extends string>(
  first: TFirst,
  second: TSecond
): [TFirst, TSecond] => {
  return [first, second];
};
const pair = createPair(1, 'a');

软件包和工具。

在应用程序开发中,普遍的做法是利用第三方工具避免不必要的工作重复。这是我在构建可扩展应用程序时使用的一些软件包。

REACT查询/Tanstack查询

React查询在复杂企业应用程序中管理数据获取和同步非常有益。它提供了一种从API,缓存和处理突变获取数据的统一方法。在企业设置中,应用程序通常需要与多个API和服务进行交互。 React查询可以通过集中数据管理和减少样板代码来简化此过程。

反应上下文

React上下文在不需要支撑钻探的情况下在各种组件上管理全球状态起有力的作用。这在企业应用程序中尤其有价值,在整个应用程序中,需要可以访问共享状态(例如用户身份验证或偏好)。

我通常保留对React上下文或其他状态管理工具的使用作为最后的手段。建议最大程度地减少对全球国家的依赖。相反,旨在使您的状态更近于特定要求的地方。

赛普拉斯是端到端测试的绝佳工具。在企业应用程序中,确保关键工作流和功能在不同的屏幕和组件上正确的功能是至关重要的。赛普拉斯是迄今为止我最喜欢的工具。每当我的测试通过时,都会相信我引入的代码不会打破应用程序。随着企业应用程序的发展,进行回归测试以捕获新代码更改的任何意外副作用至关重要。柏树通过自动化测试过程来促进这一点。

React测试库:

React测试库对于单位和反应组件的集成测试至关重要。在企业应用程序中,验证各个组件是否按预期工作对于强大的应用程序至关重要。 React测试库可以隔离地对每个组件进行彻底测试,并与其他组合结合。

Nextauth.js:

NextAuth.js简化了Next.js应用程序中身份验证和授权的实现。在企业设置中,安全用户管理是不可谈判的。企业经常采用单个登录解决方案来简化多个应用程序的用户身份验证。 NextAuth.js支持各种SSO提供商,使其非常适合企业身份验证需求。 NextAuth.js还提供了实现自定义身份验证流的灵活性。

我有一个博客here,它向您展示了如何使用模块增强的Typescript在NextAuth.js中自定义默认用户模型。

涡轮储物库

这也是我最喜欢的工具。 Turbo Repo是管理Monorepos的宝贵工具。在大型企业应用程序中,代码库可以广泛,具有各种模块,服务和共享代码。 Turbo Repo有助于组织,版本控制和部署这些代码库。在企业设置中,跨不同团队和项目共享代码很常见。 Turbo Repo可实现有效的代码共享,使团队可以在共享库和组件上进行协作。

故事书

Storybook允许开发人员隔离UI组件并在受控环境中展示它们。这使得在无需浏览整个应用程序的情况下可以轻松演示各个组件的外观和行为。在大型企业应用程序中,不同的开发人员或团队可能负责UI的不同部分。 Storybook提供了一个集中的平台,用于展示和讨论UI组件,促进有效的协作并确保一致的设计语言。 Here是我使用Storybook开发和记录的样本组件库。

在企业上下文中,这些工具共同提供了一个全面的工具包,用于构建,测试和维护大规模应用程序,以解决数据管理,状态处理,测试,身份验证和代码组织等关键方面。

可重复使用组件的编码样式

当我开发可重复使用的组件(例如输入,对话框等)时,我会尝试遵循一些最佳实践。

让我们尝试一些最佳实践来共同开发Button组件,您会发现它不仅仅是视觉设计。

组件可重复使用

确保您的按钮组件设计为可以在应用程序的不同部分重复使用。它应该足够灵活以适应各种用例。

定制道具

提供通用自定义选项的道具,例如大小,颜色,变体(例如,主要,次要)和残疾状态。这使开发人员可以轻松调整按钮以适合不同的UI上下文。

可访问性注意事项

实现适当的可访问性功能,例如ARIA-LABEL,ARIA-DISABLED和FOCUS MANAGIONS。这确保了辅助技术的用户可以有效与按钮进行交互。

语义HTML

对您的按钮组件使用语义HTML元素(例如)。这可以增强可访问性和SEO,并确保在不同设备上进行适当的行为。

模仿本地按钮元素

我们遵循的所有最佳实践都强制我们编写可预测的代码。如果您开发自定义按钮组件,请使其工作并像按钮一样行为。您将从示例组件中看到,我们将共同编写我尝试包含一个按钮可以通过扩展本机按钮来获取的所有道具。

错误处理

如果按钮可能会导致错误状态(例如,提交表单),则提供了一种将这些错误处理和传达给用户的方法。

测试

编写单元测试以验证按钮组件在不同方案中的行为是否如预期。测试用例应涵盖各种道具和事件处理程序。

文档

记录按钮组件的使用,包括可用的道具,事件处理程序和任何特定用例。提供示例和代码片段来指导开发人员。这是故事书闪耀的地方。

跨浏览器兼容性:

测试不同浏览器中的按钮组件以确保行为和外观一致。
版本控制和更改:

如果按钮组件是共享库的一部分,请实现版本控制并维护更改程序,以使开发人员了解更新和更改。

编码

对于我的组件,我通常有这样的文件。 Button.tsxButton.stories.tsxDocs.mdxButton.test.ts。如果您使用的是CSS,则可能有Button.module.css

之类的东西

components/ui/Button.tsx
这是主要组件,cn函数合并了类并处理冲突。这是tw-merge库周围的包装。

import React from 'react';
import {
  forwardRef,
  type ButtonHTMLAttributes,
  type JSXElementConstructor,
  type ReactElement,
} from 'react';
import { AiOutlineLoading3Quarters } from 'react-icons/ai';
import type { VariantProps } from 'cva';
import { cva } from 'cva';
import Link from 'next/link';
import { cn } from '@/lib';

const button = cva(
  'flex w-max items-center border-[1.5px] gap-2 transition duration-200 ease-linear focus:outline-0 focus:ring ring-offset-1 dark:ring-offset-blue-dark',
  {
    variants: {
      variant: {
        outline: '...',
        solid: '...',
        naked: '...',
      },
      rounded: {
        none: 'rounded-none',
        sm: 'rounded',
        md: 'rounded-lg',
        lg: 'rounded-xl',
        full: 'rounded-full',
      },
      color: {
        primary: '...',
        danger: '...',
        info: '...',
        warning: '...',
        light: '...',
        secondary: '...',
      },
      size: {
        xs: '...',
        sm: '...',
        md: '...',
        lg: '...',
      },
      disabled: {
        true: '...',
      },
      active: {
        true: '...',
      },
      loading: {
        true: '...',
      },
      fullWidth: {
        true: '...',
      },
      align: {
        center: '...',
        left: '...',
        right: '...',
        between: '...',
      },
    },
    compoundVariants: [
      {
        variant: 'solid',
        color: ['secondary', 'warning', 'danger', 'info'],
        className: '...',
      },
      {
        variant: 'solid',
        color: 'primary',
        className: '...',
      },
      {
        variant: 'outline',
        color: ['primary', 'secondary', 'warning', 'danger', 'info'],
        className: '...',
      },
      {
        variant: 'outline',
        color: 'light',
        className:
          '...',
      },
      {
        variant: 'naked',
        color: ['primary', 'secondary', 'warning', 'danger', 'info'],
        className:
          '...',
      },
      {
        disabled: true,
        variant: ['solid', 'outline', 'naked'],
        color: ['primary', 'secondary', 'warning', 'danger', 'info', 'light'],
        className: '...',
      },
      {
        variant: 'outline',
        color: ['primary', 'secondary', 'warning', 'danger', 'info', 'light'],
        className: '...',
      },
      {
        variant: 'naked',
        color: 'primary',
        className: '...',
      },
    ],
    defaultVariants: {
      size: 'md',
      variant: 'solid',
      color: 'primary',
      rounded: 'lg',
      align: 'center',
    },
  }
);

interface BaseProps
  extends Omit<
      ButtonHTMLAttributes<HTMLButtonElement>,
      'color' | 'disabled' | 'active'
    >,
    VariantProps<typeof button> {
  href?: string;
  loadingText?: string;
  target?: '_blank' | '_self' | '_parent' | '_top';
  as?: 'button' | 'a' | JSXElementConstructor<any>;
}

export type ButtonProps = BaseProps &
  (
    | {
        rightIcon?: ReactElement;
        leftIcon?: never;
      }
    | {
        rightIcon?: never;
        leftIcon?: ReactElement;
      }
  );

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {
    const {
      as: Tag = 'button',
      variant,
      color,
      rounded,
      size,
      target = '_self',
      loading,
      fullWidth,
      align,
      loadingText,
      href,
      active,
      rightIcon,
      leftIcon,
      className,
      disabled,
      children,
      ...rest
    } = props;

    const classes = cn(
      button({
        variant,
        color,
        size,
        disabled,
        loading,
        active,
        rounded,
        fullWidth,
        align,
      }),
      className
    );

    return (
      <>
        {href ? (
          <Link className={classes} href={href} target={target}>
            {leftIcon}
            {children}
            {rightIcon}
          </Link>
        ) : (
          <Tag className={classes} disabled={disabled} ref={ref} {...rest}>
            {loading ? (
              <>
                <AiOutlineLoading3Quarters className='animate-spin' />
                {loadingText || 'Loading...'}
              </>
            ) : (
              <>
                {leftIcon}
                {children}
                {rightIcon}
              </>
            )}
          </Tag>
        )}
      </>
    );
  }
);

Button.displayName = 'Button';

components/ui/Button.stories.tsx

此文件具有故事书的按钮故事。

import { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { FaRegSmileWink, FaThumbsUp, FaYinYang } from 'react-icons/fa';
import { FiArrowUpRight } from 'react-icons/fi';
import { Button } from './Button';

export default {
  title: 'Components/Button',
  component: Button,
  parameters: {},
  args: {
    children: 'Click me!',
  },
  argTypes: {
    children: {
      description: 'This is the text of the button, can be a node.',
      control: { type: 'text' },
    },
    color: {
      options: ['primary', 'danger', 'info', 'warning', 'secondary', 'light'],
      control: { type: 'select' },
      description: 'This controls the color scheme of the button',
      table: {
        defaultValue: { summary: 'primary' },
      },
    },
    variant: {
      options: ['solid', 'outline', 'naked'],
      control: { type: 'select' },
      description: 'This controls the variant of the button',
      table: {
        defaultValue: { summary: 'solid' },
      },
    },
    size: {
      options: ['sm', 'md', 'lg'],
      control: { type: 'radio' },
      description: 'This controls the size of the button',
      table: {
        defaultValue: { summary: 'md' },
      },
    },
    loading: {
      control: { type: 'boolean' },
      description: 'This controls the loading state of the button',
      table: {
        defaultValue: { summary: false },
      },
    },
    href: {
      control: { type: 'text' },
      description:
        'If this is set, the button will be rendered as an anchor tag.',
    },
    className: {
      control: { type: 'text' },
      description: 'Classes to be applied to the button',
    },
    disabled: {
      control: { type: 'boolean' },
      description: 'If true, the button will be disabled',
      table: {
        defaultValue: { summary: false },
      },
    },
    rightIcon: {
      options: ['Smile', 'ThumbsUp', 'YinYang'],
      mapping: {
        Smile: <FaRegSmileWink />,
        ThumbsUp: <FaThumbsUp />,
        YinYang: <FaYinYang />,
      },
      description:
        'If set, the icon will be rendered on the right side of the button',
    },
    leftIcon: {
      options: ['Smile', 'ThumbsUp', 'YinYang'],
      mapping: {
        Smile: <FaRegSmileWink />,
        ThumbsUp: <FaThumbsUp />,
        YinYang: <FaYinYang />,
      },
      description:
        'If set, the icon will be rendered on the left side of the button',
    },
    loadingText: {
      control: { type: 'text' },
      description:
        'If set, the text will be rendered while the button is in the loading state',
    },
    target: {
      control: { type: 'text' },
      description:
        'If set, the target will be rendered as an attribute on the anchor tag',
      table: {
        defaultValue: { summary: '_self' },
      },
    },
    as: {
      options: ['button', 'a'],
      control: { type: 'select' },
      description:
        'If set, the button will be rendered as the specified element',
      table: {
        defaultValue: { summary: 'button' },
      },
    },
  },
} as Meta<typeof Button>;

type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {},
};

export const Secondary: Story = {
  args: {
    color: 'secondary',
  },
};

export const Danger: Story = {
  args: {
    color: 'danger',
  },
};

export const Warning: Story = {
  args: {
    color: 'warning',
  },
};

export const Light: Story = {
  args: {
    color: 'light',
  },
};

export const Info: Story = {
  args: {
    color: 'info',
  },
};

export const Custom: Story = {
  args: {
    className: 'bg-[yellow] text-[black] border-[orange]',
    style: { borderRadius: '3.5rem' },
  },
};

export const WithRightIcon: Story = {
  args: {
    rightIcon: <FiArrowUpRight className='h-5 w-auto' />,
  },
};

export const WithLeftIcon: Story = {
  args: {
    leftIcon: <FiArrowUpRight className='h-5 w-auto' />,
  },
};

export const Disabled: Story = {
  args: {
    disabled: true,
  },
};

export const OutlineVariant: Story = {
  args: {
    variant: 'outline',
    color: 'danger',
  },
};

export const NakedVariant: Story = {
  args: {
    variant: 'naked',
    color: 'danger',
  },
};

export const Loading: Story = {
  args: {
    loading: true,
  },
};

export const CustomLoadingText: Story = {
  args: {
    loading: true,
    loadingText: 'Processing...',
  },
};

export const AsLink: Story = {
  args: {
    href: 'https://fin.africa',
    children: 'Visit fin website',
    rightIcon: <FiArrowUpRight className='h-5 w-auto' />,
  },
};

export const FullWidth: Story = {
  args: {
    fullWidth: true,
    children: 'Visit fin website',
    rightIcon: <FiArrowUpRight className='h-5 w-auto' />,
  },
};

components/ui/Docs.mdx

故事文件可以记录组件的工作原理,但是降价文件可以具有更广泛的文档。

我用来开发Button组件的惯例是我尝试遵循所有组件的惯例。

关键要点

  • 具有某种形式的设计系统,天气是一种开源解决方案,或者您自己旋转。

  • 使打字稿成为朋友。利用您的优势,请使用它来强制您希望人们如何消耗组件。一个很好的例子是我们的按钮组件。它有2个道具leftIconrightIcon。我们已经使用Typescript来确保仅设置其中一个,否则它会给开发人员错误。

export type ButtonProps = BaseProps &
  (
    | {
        rightIcon?: ReactElement;
        leftIcon?: never;
      }
    | {
        rightIcon?: never;
        leftIcon?: ReactElement;
      }
  );
  • 记录您的代码和组件。使用Storybook等工具。

  • 有某种样式指南,以确保您与团队说相同的语言。

  • 写转储代码。保持您的代码库直接并专注。每个代码都应该有一个清晰的目的。

  • 了解事物在引擎盖下的工作方式。我发现反应如何检查两个值是否相同后发表了一篇文章here

结论

我们探索了我使用的一些方法和工具。尽管我尚未涵盖所有可以使用的工具,但建议您确定哪些适合您的特定要求。建议坚持使用您熟练的技术,而不是仅仅为了新颖而采用东西。

最终,客户最关心的是最终产品,而不是您采用的特定技术。无论是反应,vue还是其他工具,都优先使用工具和工作流,这些工具和工作流程使您的用户有利于您的快速部署。

资源