搬砖 React 4 年,我总结了这些企业级应用的要点

首发于公众号 前端从进阶到入院,欢迎关注。

在快节奏的前端开发领域,牢牢掌握技术发展前沿对搭建成功的企业级应用至关重要。在使用 Next.js 及其强大的技术栈(包括 Tailwind CSS、TypeScript、TurboRepo、ESLint、React Query 等)长达四年后,我已经积累了许多宝贵的见解和最佳实践,希望与其他开发者分享。本文将探讨如何为大规模企业构建和组织前端应用,以达到性能、可维护性和可扩展性最大化。

注意:本文表达个人观点,我提倡的方法可能不适用于您的具体情况。

有效的企业级前端架构的指导原则

在为企业级应用构建前端解决方案时,有一个明确定义的原则集可以作为指导你的发展方向的罗盘。在此节中,我会分享在企业环境中使用 Next.js 所积累的原则。

模块化和组件化

原则:分而治之

在庞大的企业级应用领域,代码可以迅速变成一头难以驯服的野兽。拥抱模块化和组件化,将你的前端拆分成可管理的部件。把组件想象为乐高积木,每个都服务于特定目的。这不仅增强了代码的可重用性,也简化了你的开发团队内部的维护和协作。不要只考虑将应用分割成更小的组件,也要考虑将其拆分成更小的独立应用。这是 Turbo Repo 等工具大显身手的地方。

关注点分离(SoC)

原则:保持代码整洁

为了维持代码的可理解性,遵循关注点分离(SoC)原则。确保你的组件专注于各自的责任,无论是渲染 UI、处理业务逻辑还是管理状态。这种隔离不仅使代码更易于理解,还有利于测试和调试。

可扩展性设计

原则:规划未来增长

企业应用不是静态的,它们在演进。在设计前端架构时就考虑到可扩展性。这意味着选择能适应流量、数据量和功能复杂性增长的模式和工具。Next.js 的面向可扩展性的设计可以成为这项努力中的宝贵帮手。

可维护性和代码质量

原则:精心编写

代码是你产品的基石。从第一天就优先考虑可维护性和代码质量。实施编码标准,进行代码审查,并投资于自动化测试。一个维护良好的代码库不仅更易于使用,也更少 Bug 和回归。我最近在工作中开发了一个组件库和一个基本的风格指南来规范我们的前端应用。请不要介意文档,它们还未完成 😂。

默认可访问性

原则:从一开始就行动

可访问性是现代 Web 开发的必需品。从一开始就将其作为默认实践。确保你的应用可被所有人使用,无论是否残疾。利用 Next.js 对可访问性标准和工具的支持来创建包容的用户体验。我使用像 Radix UI 这样的工具来构建一些需要可访问性的组件,如标签页、下拉菜单等。

面向性能的开发

原则:速度至关重要

企业用户期待快速的体验。在每一个决定点都优先考虑性能。优化资源,最小化不必要的请求,并利用 Next.js 的性能特性,如自动代码拆分、suspense 流加载和图像优化。一个快速的应用不仅取悦用户,还对 SEO 有积极影响。

安全至上

原则:守卫你的城堡

安全应该贯穿你的前端架构的方方面面。防范常见的漏洞,如跨站脚本(XSS)和跨站请求伪造(CSRF)。保持警惕,采用安全更新和最佳实践,并考虑 Next.js 内置的安全特性作为额外的防线。

国际化 (i18n) 和本地化 (l10n)

原则:全球思考

在这个互联的世界,全球化思维至关重要。从一开始就实施国际化(i18n)和本地化(l10n)以适应不同的用户群。Next.js 为这些特性提供了优秀的支持,使创建多语言应用更容易。

这些指导原则构成了使用 Next.js 构建企业级前端架构的基石。它们发挥指南针的作用,确保你的开发工作符合大规模应用的需求,使其健壮、可维护且对用户友好。在以下章节中,我们将深入探讨这些原则如何转化为可执行的策略和最佳实践。

文件夹和文件结构

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

erlang 复制代码
├─ 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/components : 这个目录包含你的 UI 组件。它被进一步细分为 ui 来存放通用 UI 组件,和 shared 来存放在应用不同部分可能被重用的组件。

  • src/modules: 这个目录存放你应用的不同模块或页面。每个模块可能有自己的文件夹,包含 API 调用、组件和工具函数的子目录。

  • src/pages: 如果你使用 Next.js,这个文件夹应该只用于作为应用的入口。不应在这里存放业务逻辑。pages 文件夹中的组件应该只渲染来自 modules 文件夹的页面。

  • src/modules/ProductsPage : 这个模块与产品相关,包含用于 API 调用、组件(如 ProductItemProductsStatistics)和工具函数(filterProductsByType)的子目录。

  • src/lib : 这个文件夹可能包含后期可以转换为在多个应用中使用的包的实用工具函数。它不同于 src/utils,后者可能包含不适合后期转为包的工具函数。

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

  • src/public : 这个文件夹包含不经过构建过程的静态资源。可能包括图片、字体和 index.html 文件。

  • src/consts , src/types: 这些目录分别可能包含常量和 TypeScript 类型定义。

  • src/hooks: 这个目录可能存放在整个应用中使用的自定义 Hooks。

  • eslintrc.js: 这是一个 ESLint 配置文件,ESLint 是一款流行的 JavaScript linting 工具。它用于实施编码约定和捕获代码中的潜在错误。

tsconfig 文件配置后,如果你想导入一个 Button 组件,可以这样导入:import { Button } from '@/components/ui'。下面是 tsconfig.json 中的配置片段。

json 复制代码
{
  ...
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

TypeScript 编码约定

我遵循的约定受此指南的启发。我强烈推荐你阅读它,下面的代码片段也出自该指南。

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

ts 复制代码
// ❌ 除非需要扩展或实现,避免接口定义
interface UserRole = 'admin' | 'guest';

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

// ✅ 使用类型定义
type UserRole = 'admin' | 'guest';

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

避免使用多个参数

ts 复制代码
// ❌ 避免有多个参数
transformUserInput('client', false, 60, 120, null, true, 2000);

// ✅ 使用选项对象作为参数
transformUserInput({
  method: 'client',
  isValidated: false,
  minLines: 60,
  maxLines: 120,
  defaultInput: null,
  shouldLog: true,
  timeout: 2000,
});

命名约定

尽管确定最佳命名可能具有挑战性,但请尽量遵循确立的约定来增强代码可读性并为未来的开发者保持一致性:

变量

  • 局部变量 驼峰式 productsproductsFiltered
  • 布尔值 前缀为 ishasisDisabledhasProduct
  • 常量 大写 PRODUCT_ID
  • 对象常量 唯一的,大小写加断言和可选 satisfies 类型(如果可用的话,TS 4.9 可用)。
ts 复制代码
const ORDER_STATUS = {
  pending: 'pending',
  fulfilled: 'fulfilled',
  error: 'error',
} as const satisfies OrderStatus;

函数

驼峰式: filterProductsByTypeformatCurrency

泛型

名称以大写字母 T 开头 TRequestTFooBar(类似 .Net 内部实现)。

避免(常见约定)用一个字符命名泛型 TK 等,我们引入的变量越多,就越容易混淆。

ts 复制代码
// ❌ 用一个字符命名泛型
const createPair = <T, K extends string>(first: T, second: K): [T, K] => {
  return [first, second];
};
const pair = createPair(1, 'a');

// ✅ 以大写字母 T 开头
const createPair = <TFirst, TSecond extends string>(
  first: TFirst,
  second: TSecond
): [TFirst, TSecond] => {
  return [first, second];
};
const pair = createPair(1, 'a');

包和工具

在应用开发中,利用第三方工具来避免不必要的重复工作是常见做法。下面是我在构建可扩展应用时使用的一些包。

React Query/Tanstack Query

React Query 在管理复杂企业应用中的数据获取和同步方面非常有益。它提供了从 API 获取数据、缓存和处理变更的统一方式。在企业环境下,应用通常需要与多个 API 和服务进行交互。React Query 可以通过集中化数据管理和减少样板代码来简化这个过程。

React Context

React Context 在通过各组件管理全局状态方面发挥重要作用,无需 prop drilling。这在共享状态(如用户认证或偏好设置)需要在整个应用中可访问的企业应用中特别有价值。

我通常只把 React Context 或其他状态管理工具作为最后手段。建议尽量减少对全局状态的依赖。而是将状态保存在更接近其所需的具体位置。

Cypress

Cypress 是端到端(E2E)测试的优秀工具。在企业应用中,确保不同屏幕和组件上的关键流程和功能正常运行至关重要。Cypress 是迄今为止我最喜欢的工具。每当我的测试通过时,这能让我确信我引入的代码没有破坏应用。随着企业应用的发展,进行回归测试以捕获任何新代码变更的意外副作用至关重要。Cypress 通过自动化测试过程来实现这一点。

React Testing Library

React Testing Library 是对 React 组件进行单元和集成测试的必备之物。在企业应用中,验证各个组件的预期工作方式对健壮的应用非常关键。React Testing Library 允许彻底测试每个组件的隔离情况,以及与其他组件的结合情况。

NextAuth.js

NextAuth.js 简化了在 Next.js 应用中实现认证和授权。在企业环境中,安全的用户管理势在必行。企业通常采用单点登录(SSO)解决方案,在多个应用中简化用户认证。NextAuth.js 支持各种 SSO 提供商,非常适合企业认证需求。NextAuth.js 还提供实现自定义认证流程的灵活性。

我在这篇博客中展示了如何使用 TypeScript 的模块扩展自定义 NextAuth.js 中的默认 User 模型。

Turbo Repo

这也是我最喜爱的工具。Turbo Repo 是管理 monorepo 的高价值工具。在大型企业应用中,代码库可以非常庞大,包含不同的模块、服务和共享代码。Turbo Repo 可以高效地组织、版本控制和部署这些代码库。在企业环境中,跨不同团队和项目的代码共享很常见。Turbo Repo 实现了有效的代码共享,允许团队在共享库和组件上进行协作。

Storybook

Storybook 允许开发者隔离 UI 组件并在可控环境中展示它们。这使得演示单个组件的外观和行为变得很容易,而无需浏览整个应用。在大型企业应用中,不同的开发人员或团队可能负责 UI 的不同部分。Storybook 提供了展示和讨论 UI 组件的集中平台,促进高效协作并确保一致的设计语言。这里是一个我使用 Storybook 开发和文档化的示例组件库。(这还在开发中)

在企业环境下,这些工具共同提供了一个全面工具包,用于构建、测试和维护大规模应用,解决数据管理、状态处理、测试、认证和代码组织等关键方面。

编写可重用组件的编码风格

在开发诸如输入框、对话框等可重用组件时,我尽量遵循一些最佳实践。

让我们一起尝试为 Button 组件开发一些最佳实践,你会发现这不仅仅是视觉设计。

组件重用性

确保你的按钮组件被设计成可以在应用不同部分重用。它应该足够灵活以适应不同的使用场景。

定制属性

提供常见定制选项的属性,如大小、颜色、变体(例如主要、次要)和禁用状态。这使得开发者可以轻松地将按钮调整为不同的 UI 上下文。

可访问性

正确的可访问性功能,如 aria-label、aria-disabled 和焦点管理,可以确保辅助技术的用户可以有效地与按钮进行交互。

语义化 HTML

为你的按钮组件使用语义化 HTML 元素(例如 <button>)。这增强了可访问性和 SEO,并确保在不同设备上表现出正确的行为。

模仿原生按钮元素

我们遵循的所有最佳实践都督促我们编写可预测的代码。如果你开发一个自定义按钮组件,请确保它的工作方式和行为像一个按钮。你会从我们一起编写的示例组件中看到,我试图通过扩展原生按钮元素来包含按钮可以接受的所有属性。

错误处理

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

测试

编写单元测试以验证按钮组件在不同场景下的预期行为。测试用例应覆盖不同的属性和事件处理程序。

文档

记录按钮组件的使用方式,包括可用属性、事件处理程序和任何特定使用场景。提供示例和代码片段以指导开发者。这是 Storybook 的强项。

跨浏览器兼容性

在不同浏览器中测试按钮组件,以确保行为和外观的一致性。

版本控制和变更日志

如果按钮组件是共享库的一部分,请实施版本控制并维护变更日志,以让开发者了解更新和更改。

编码

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

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

jsx 复制代码
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

这个文件包含 Storybook 中按钮的 stories。

jsx 复制代码
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

stories 文件可以记录组件的工作方式,但 markdown 文件可以包含更广泛的文档。

我使用的开发 Button 组件的约定,也是我尝试在所有组件上遵循的约定。

关键要点

  • 采用某种设计系统,无论是开源解决方案还是你自己启动的。

  • 充分利用 TypeScript。使用 TypeScript 发挥优势,用它来约束人们如何使用你的组件。一个很好的例子是我们的 Button 组件。它有两个属性 leftIconrightIcon。我们使用 TypeScript 确保只设置其中一个,否则会向开发者报错。

ts 复制代码
export type ButtonProps = BaseProps &
  (
    | {
        rightIcon?: ReactElement,
        leftIcon?: never,
      }
    | {
        rightIcon?: never,
        leftIcon?: ReactElement,
      }
  );
  • 记录你的代码和组件。使用像 Storybook 这样的工具。

  • 有某种风格指南以确保你和你的团队说同一种语言。

  • 编写简单的代码。保持你的代码库直接和关注点单一。每段代码都应该有单一、清晰的目的。

  • 了解底层的工作原理。在了解 React 如何检查两个值是否相同后,我发表了一篇文章。

结论

我们探讨了我使用的一些方法和工具。虽然我没有涵盖我所有工具,但我建议确定什么适合你的特定要求。最好坚持你熟练的技术,而不是仅因新颖而采用某项技术。

归根结底,客户最关心的是最终产品,而不是你使用的特定技术。无论是 React、Vue 还是其他工具,都要优先使用那些能够快速部署以造福用户的工具和工作流程。

资源

参考来源:dev.to/josemukoriv...

首发于公众号 前端从进阶到入院,作者 ssh,工作 6 年+,阿里云、字节跳动 Web infra 一线拼杀出来的资深前端工程师 + 面试官,非常熟悉大厂的面试套路,Vue、React 以及前端工程化领域深入浅出的文章帮助无数人进入了大厂,关注后回复「指南」,获取高级前端、算法学习路线,是我自己一路走来的实践。

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax