找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2419

积分

1

好友

333

主题
发表于 昨天 16:31 | 查看: 2| 回复: 0

本文整理了一位开发者在使用 React、TypeScript 及 Vite 等技术栈进行项目开发过程中,所遇到的实际问题及其解决方案。内容覆盖了从包管理、项目配置、CSS 技巧到部署运维等多个方面,旨在为前端开发者提供一份实用的参考手册。

1. package.json中^~版本符号的区别

package.json 文件中,^(尖角号)和 ~(波浪线)用于定义不同的版本更新范围。

  • ^(尖角号):允许更新次要版本和修订版本,但保持主版本号不变。例如,"^1.2.3" 允许安装 >=1.2.3<2.0.0 的最新版本。
  • ~(波浪线):允许更新修订版本,但保持次要版本不变。例如,"~1.2.3" 允许安装 >=1.2.3<1.3.0 的最新版本。

正确使用这些符号有助于在保持依赖兼容性的同时,及时获取安全补丁和功能更新。

2. npm install 时遭遇版本冲突的解决方法

执行 npm install 时,可能会遇到如下错误:

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: panda@1.0.0
...

此错误通常由依赖树中的版本冲突引起,可以尝试以下解决方案:

  1. 删除 node_modules 文件夹和 package-lock.json 文件,然后重新执行 npm install
  2. 使用 npm install --legacy-peer-deps 命令,该命令会忽略对等依赖(peerDependencies)的版本限制。
  3. 检查并更新 package.json 中的依赖版本,确保其符合语义化版本规范。
  4. 尝试更换包管理器,如 yarnpnpm,再执行安装。

3. 快速定位项目中特定组件的位置

在一个大型项目中,快速找到某个组件对应的文件是常见需求。可以遵循以下步骤:

  1. 在浏览器中运行项目,从地址栏找到与组件相关的路由关键字。
  2. 在代码编辑器中,复制项目路由配置文件的相对路径。
  3. 结合组件关键字与路由路径,在项目中全局搜索,即可快速定位到定义该组件的路由项。
  4. 通过路由配置,便能清晰地看到组件定义的具体文件位置。

兑换码管理界面截图
兑换码管理页面

VS Code右键菜单截图
在文件管理器中复制路径

VS Code全局搜索路由组件
通过路由关键字全局搜索

路由配置文件TypeScript代码
路由配置文件中定义的组件路径

4. CSS模块化的常见方案

CSS模块化有助于管理样式、避免冲突和提高代码可维护性,主要有以下几种方案:

  1. 命名约定:如使用 BEM(Block, Element, Modifier)方法论。这种方式简单直观,但随着项目规模增大,仍可能出现命名冲突和代码重复。
  2. CSS Modules:一种官方推荐的模块化方案,利用构建工具(如Webpack, Vite)自动为类名生成唯一哈希,实现样式隔离。在React项目中,常以.module.less.module.css等形式使用。缺点是学习成本稍高,且需要借助构建工具。
  3. CSS-in-JS:将CSS样式作为JavaScript对象嵌入到组件中,例如使用 Styled Components 或 Emotion 库。这种方式能更好地利用JavaScript的动态能力,实现组件化样式,但会引入额外的库依赖。

5. TypeScript接口/类型的命名规范

为了保持代码的一致性和可读性,定义TypeScript类型时建议遵循一定的规范。一种常见的约定是:使用大写字母 I 开头,采用帕斯卡命名法(每个单词首字母大写)。如果类型表示一个数组项,可以在名称后加上 Item

// 示例
export interface IOperateInfoItem {
  action: string
  name: string
  createTime: string
  type: string
  docnumber: number
}

6. git clone 项目后 npm install 失败的权限问题

从远程仓库克隆项目后,执行 npm install 可能因权限问题失败。

npm install权限错误
npm install 权限错误

原因:通常是 node_global 等目录的访问权限不足。
解决

  1. 为相关目录(如 node_global, node_cache)添加适当的写入权限。
  2. 如果仍有部分包因“预依赖”安装失败,可以尝试使用 npm install -f 进行强制安装。

npm依赖解析冲突
依赖冲突导致安装失败

7. 代码注释的技巧:// vs /** */

在TypeScript/JavaScript中,注释的写法会影响IDE的智能提示。

使用 // 单行注释时,变量在其他地方使用时,鼠标悬停可能不会显示注释内容:

// 操作人日志前端记录在access里面的人名+工号
const local = {
  name: access.curAccount.account,
  workcode: access.curAccount.workcode,
}

而使用 /** */ 形式的JSDoc注释时,在其他地方引用该变量,IDE通常会提供完整的注释提示,大大提升了代码的可读性和开发体验。

/** 操作人日志前端记录在access里面的人名+工号 */
const local = {
  name: access.curAccount.account,
  workcode: access.curAccount.workcode,
}

8. 泛型在分页接口类型定义中的应用

在定义后端分页接口的返回类型时,data 字段的结构(如 current, pages, records, total 等)通常是固定的,但 records 中的具体数据项(T)会变化。这时,使用泛型可以优雅地解决这个问题。

首先,定义一个通用的分页响应接口:

/** 公共的分页接口响应范型 */
export interface PageSuccessResponse<T> {
  current: number
  pages: number
  records: T[]
  searchCount: true
  size: number
  total: number
}

然后,在具体的业务接口中,传入对应的数据项类型即可:

// 兑换码管理返回数据项类型
export interface redemptionCodeManageList {
  distributorName: string
  distributorGrade: string
  distributorId: number
  exchangeCode: string
  createdTime: string
  goodsId: number
  goodsName: string
  goodsType: number
  orderNo: number
  status: number
}

// API请求函数
export function getRedemptionCodeManageData(params: redemptionCodeManageParams) {
  const option = {
    method: 'POST',
    data: params,
  }
  // 使用泛型,明确records的具体类型
  return request<PageSuccessResponse<redemptionCodeManageList>>(
    '/distribute/panda/exchange/findByPage',
    option,
  )
}

9. 企业级项目的自动化部署流程

利用GitLab的Webhook功能可以实现代码提交后自动触发部署,基本流程如下:

  1. 配置Webhook:在GitLab项目的设置中,添加一个新的Webhook,URL指向部署服务器上用于接收钩子请求的脚本地址。
  2. 编写部署脚本:在部署服务器上编写脚本(如Shell、Python脚本),该脚本接收Webhook的POST请求,解析其中的信息(如分支、提交记录),并触发后续的自动化流程。
  3. 设计部署流程:流程可以包括拉取最新代码、运行测试、执行构建(Build)、将构建产物部署到服务器等步骤。可以使用Jenkins、GitLab CI/CD或Ansible等工具来编排和管理这个流程。

配置完成后,每次向指定分支(如main)推送代码,都会自动触发完整的部署流程,实现持续集成与持续部署。

10. git clone时SSH权限问题的解决

首次使用SSH方式克隆Git仓库时,可能会遇到权限被拒绝的错误。

Git克隆权限错误
SSH克隆权限错误

解决方案:在本地生成SSH密钥对,并将公钥添加到Git服务器(如GitLab、GitHub)的账户设置中。

生成SSH密钥步骤

  1. 打开终端。
  2. 运行命令:ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
  3. 连续按回车,接受默认文件位置,并可选择设置密码。
  4. 生成后,在 ~/.ssh/ 目录下会得到 id_rsa(私钥)和 id_rsa.pub(公钥)两个文件。

生成SSH密钥对
在终端生成SSH密钥对

  1. 复制 id_rsa.pub 文件的内容,将其粘贴到Git服务器个人设置中的“SSH Keys”页面。

GitLab添加SSH公钥界面
在GitLab中添加SSH公钥

添加成功后,即可正常使用SSH克隆和推送代码。

11. 使用<a>标签的安全注意事项

在使用 <a> 标签打开外部链接时,除了设置 href,强烈建议同时设置以下两个属性:

<a href="https://example.com" target="_blank" rel="noopener noreferrer">访问示例</a>
  • target="_blank":在新标签页中打开链接。
  • rel="noopener noreferrer":这是一个重要的安全属性组合。
    • noopener 防止新打开的页面通过 window.opener 访问原页面,避免潜在的安全风险。
    • noreferrer 阻止浏览器在请求新页面时发送 Referer 头,保护原页面的URL信息不被泄露。

养成使用这个属性组合的习惯,可以有效防范钓鱼攻击和隐私泄露。

12. 使用 postcss-pxtorem 实现 px 到 rem 的自动转换

为了适配不同屏幕,我们常常需要将设计稿的 px 单位转换为 rem。postcss-pxtorem 插件可以自动完成这项工作。

1. 安装依赖

pnpm install postcss-pxtorem

2. 配置 postcss.config.js

export default {
  plugins: {
    'postcss-pxtorem': {
      // 设计稿宽度为1920px时的基准值,通常设为设计稿宽度/10
      rootValue: 192,
      // 转换后rem值的小数点位数
      unitPrecision: 2,
      // 需要转换的属性列表,* 代表所有属性
      propList: ['*'],
      // 排除不需要转换的文件,如 node_modules 下的样式
      exclude: function (file) {
        return file.indexOf('node_modules') > -1;
      },
    },
  },
};

3. 动态设置根字体大小
在项目入口文件(如 main.tsx)中,添加根据屏幕宽度动态计算 html 字体大小的逻辑。

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from '@/App';
import '@/assets/global.less';

const onResize = () => {
  let width = document.documentElement.clientWidth;
  // 设定最大宽度限制
  if (width > 1920) {
    width = 1920;
  }
  // 设置根字体大小 (1920px设计稿时,1rem = 192px)
  document.documentElement.style.fontSize = width / 10 + 'px';
};

// 初始化及监听窗口变化
onResize();
window.addEventListener('resize', onResize);

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.Fragment>
    <App />
  </React.Fragment>,
);

配置完成后,在 CSS 中直接书写设计稿的 px 值,插件会自动将其转换为 rem,从而实现响应式布局。

13. 自定义 Ant Design 组件样式的技巧

使用 Ant Design 组件时,经常需要覆盖其默认样式。关键在于找到正确的 CSS 类名并进行覆盖。

全局修改:如果你想修改项目中所有同类组件的样式,可以在样式文件中使用 :global 包裹覆盖规则。

:global {
  .ant-dropdown .ant-dropdown-menu {
    box-shadow: 0px 7.41667px 22.25px rgba(54, 88, 255, 0.15);
    border-radius: 14.8333px;
    padding: 20px 10px 20px 10px;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
}

局部修改:如果只想修改某个特定实例的样式,可以为组件添加 rootClassNameclassName 属性,然后针对这个自定义类名编写样式。

// 组件使用
<Dropdown
  rootClassName={styles.dropdown} // 传入自定义类名
  open={true}
  menu={{ items: about_items }}
  placement="bottom"
>
  <a><Space>{home.nav.middleNav.title}<DownOutlined /></Space></a>
</Dropdown>
// 样式文件
.dropdown {
  :global {
    .ant-dropdown-menu {
      box-shadow: 0px 7.41667px 22.25px rgba(54, 88, 255, 0.15);
      border-radius: 14.8333px;
      padding: 20px 10px 20px 10px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      li {
        padding: 4.8px 36px 4.8px 36px !important;
      }
    }
  }
}

14. 屏幕适配与布局常见问题

14-1 避免使用绝对定位进行整体布局

在还原设计稿时,组件最外层的容器(container)不应设置固定宽高,而应使用 width: 100%;max-width: [设计稿最大宽度]px;,让内部子元素自然撑开高度。

14-2 处理意外出现的滚动条

滚动条意外出现,通常是由于元素宽度计算溢出导致的。一个常见原因是:没有显式设置宽度的元素,其默认宽度为100%。如果同时为其设置了 leftright 偏移,就会导致实际宽度超过父容器。

解决方案:使用 calc() 函数精确计算元素宽度。

/* 错误:默认宽度100% + left偏移 => 溢出 */
.booksBox {
  position: relative;
  left: 160px;
  div {
    display: inline-block;
    width: 240px;
    margin-right: 54px;
  }
}

/* 正确:使用 calc 控制总宽度 */
.booksBox {
  position: relative;
  left: 160px;
  width: calc(100% - 160px); /* 减去偏移量 */
  div {
    display: inline-block;
    width: 240px;
    margin-right: 54px;
  }
}

14-3 应对浏览器的12px字体限制

在页面缩小时,固定宽高盒子内的文字可能因浏览器最小字体限制(通常为12px)而溢出。

解决方案

  1. 使用内边距(padding)替代固定宽高:让内容(文字)撑开盒子。
  2. 使用媒体查询缩放:当屏幕缩小时,强制缩放字体。注意,缩放 (transform: scale()) 会影响整个元素,通常需要为文字额外包裹一层元素进行缩放。
@media screen and (max-width: 1920px) {
  .textContainer {
    font-size: 12px;
    transform: scale(0.6);
  }
}

14-4 理解 viewport meta 标签

<meta name=”viewport” content=”width=device-width, initial-scale=1.0”> 对于移动端适配至关重要。它告诉浏览器,页面的宽度应该等于设备的宽度,并且初始缩放比例为1.0。如果没有这个标签,移动设备上的页面可能会被错误地缩放,导致体验不佳。在没有专门移动端设计稿时,有时移除它反而能防止复杂桌面布局在移动端崩溃。

15. Grid 布局实战案例:不规则图片墙

以下是一个使用 CSS Grid 创建不规则图片排列布局的案例,展示了 Grid 强大的二维布局能力。

JSX 结构

<div className={styles.innerface}>
  <div className={styles.imageList}>
    {imageList.map((imgSrc, index) => (
      <div className={styles.item} key={index}>
        <img src={imgSrc} alt="" />
      </div>
    ))}
  </div>
</div>

CSS (Less) 样式

.innerface {
  width: 1920px;
  height: 1024px;
  position: absolute;
  top: 3750px;
  left: 50%;
  transform: translate(-50%, 0);
  display: flex;
  justify-content: center;
  align-items: center;

  .imageList {
    display: grid;
    // 创建8列等宽网格
    grid-template-columns: repeat(8, 1fr);
    // 创建3行,高度自动
    grid-template-rows: repeat(3, auto);
    gap: 10px;
    width: 100%;
    height: 100%;
    opacity: 0.15;

    // 针对第1列和第8列(每行的首尾项)设置特殊样式
    .item:nth-child(8n + 1),
    .item:nth-child(8n) {
      img {
        width: calc(50%);
        height: calc((100%) - 10px);
      }
    }
    .item:nth-child(8n) {
      text-align: right;
    }

    // 针对中间6列(第2-7列)的项,使用绝对定位微调图片位置
    .item:not(:nth-child(8n + 1)):not(:nth-child(8n)) {
      position: relative;
      img {
        position: absolute;
        width: calc((100%));
        height: calc((100%) - 10px);
      }
    }
    // 为中间每一项设置不同的水平偏移,形成交错感
    .item:nth-child(8n + 2) img { left: -75px; }
    .item:nth-child(8n + 3) img { left: -45px; }
    .item:nth-child(8n + 4) img { left: -15px; }
    .item:nth-child(8n + 5) img { right: -15px; }
    .item:nth-child(8n + 6) img { right: -45px; }
    .item:nth-child(8n + 7) img { right: -75px; }
  }
}

核心思路:利用 Grid 划分基础网格结构,然后通过 :nth-child 选择器精准定位每一个网格项,对特定位置的项(如每行的首尾)应用不同的宽度,对中间的项使用绝对定位进行精细的视觉调整,最终形成富有设计感的布局效果。

Grid布局实现的图片墙效果
Grid布局实现的交错图片墙

16. React项目实现多语言切换(i18n)

多语言是国际化网站的标配。下面介绍一种基于 React Context 和自定义 Hook 的实现方案。

1. 封装存储工具:首先,封装一个操作 sessionStorage 的工具类,用于持久化用户的语言选择。

// storage.ts
export const localStorageKey = 'com.drpanda.chatgpt.';

export class Storage<T> {
  key: string;
  defaultValue: T;
  constructor(key: string, defaultValue: T) {
    this.key = localStorageKey + key;
    this.defaultValue = defaultValue;
  }
  setItem(value: T) {
    sessionStorage.setItem(this.key, JSON.stringify(value));
  }
  getItem(): T {
    const value = sessionStorage[this.key] && sessionStorage.getItem(this.key);
    if (value === undefined) return this.defaultValue;
    try {
      return value && value !== 'null' && value !== 'undefined'
        ? (JSON.parse(value) as T)
        : this.defaultValue;
    } catch (error) {
      return value && value !== 'null' && value !== 'undefined'
        ? (value as unknown as T)
        : this.defaultValue;
    }
  }
}

/** 管理语言选项 */
export const localeStorage = new Storage<ILocale>('locale', undefined as unknown as ILocale);

2. 创建状态管理模型:创建一个自定义 Hook (useLocales) 来管理语言状态。它从存储中读取语言设置,或根据浏览器语言自动判断默认值,并提供切换语言的方法。

// locales model
import enUS from '@/locales/en-US';
import esES from '@/locales/es-ES';
import { Storage } from '@/common/storage';
import { useMemo, useState } from 'react';

// 获取浏览器默认语言
const getBrowserLanguage = () => {
  const languageString = navigator.language || navigator.languages[0];
  const [language] = languageString.split('-');
  return language;
};

const localesMap = {
  enUS,
  esES,
  default: getBrowserLanguage() === 'es' ? esES : enUS,
};

type ILocale = 'enUS' | 'esES' | 'default';
export const localeStorage = new Storage<ILocale>('locale', undefined as unknown as ILocale);

export default () => {
  // 从storage读取或使用默认值
  const [locale, _setLocale] = useState<ILocale>(localeStorage.getItem() || 'default');
  // 根据当前locale获取对应的语言包
  const locales = useMemo(() => (locale ? localesMap[locale] : localesMap.default), [locale]);
  // 切换语言,并更新storage
  const setLocale = (value: ILocale | ((value: ILocale) => ILocale)) => {
    if (typeof value === 'function') {
      value = value(locale!);
    }
    localeStorage.setItem(value);
    _setLocale(value);
  };
  return {
    ...locales, // 语言包内容
    locale,     // 当前语言key
    setLocale,  // 切换函数
  };
};

3. 集成到全局状态:利用一个轻量的状态管理库(基于 Context),将语言模型注入到整个应用。

// store/index.ts
import createStore from './createStore'; // 一个自定义的createContext封装
import locales from './modules/locales';

const store = () => ({
  locales: locales(),
});
const contextResult = createStore(store);
export const { useModel, StoreProvider } = contextResult;

4. 在组件中使用:在需要切换语言或使用翻译内容的组件中,通过 useModel 获取状态和方法。

// HomePage组件片段
import { useModel } from '@/store';
import { Dropdown, Space } from 'antd';
import { DownOutlined } from '@ant-design/icons';

const HomePage = () => {
  const { home, setLocale } = useModel('locales'); // home是语言包中的对象

  const language_items = [
    { label: 'English', key: '1' },
    { label: 'Español', key: '2' },
  ];

  const onLanguageClick = ({ key }) => {
    setLocale(key === '1' ? 'enUS' : 'esES');
  };

  return (
    <div>
      <Dropdown menu={{ items: language_items, onClick: onLanguageClick }}>
        <a onClick={(e) => e.preventDefault()}>
          <Space>
            Language
            <DownOutlined />
          </Space>
        </a>
      </Dropdown>
      <h1>{home.welcomeTitle}</h1> {/* 使用翻译文本 */}
    </div>
  );
};

网站语言切换下拉菜单
网站顶部的语言切换下拉菜单

17. 基于 Fetch 封装支持拦截器的请求库

现代前端应用通常需要对网络请求进行统一管理。下面展示如何基于 fetch 封装一个带有请求和响应拦截器的通用请求函数。

基础封装

// request.ts
export interface IRequestOptions {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: { [key: string]: string };
  body?: BodyInit;
}

export async function request<T>(url: string, options: IRequestOptions = {}): Promise<T> {
  const response = await fetch(url, {
    method: options.method || 'GET',
    headers: options.headers || { 'Content-Type': 'application/json' },
    body: options.body,
  });
  if (!response.ok) {
    throw new Error(`Request failed with status code ${response.status}`);
  }
  const data = (await response.json()) as T;
  return data;
}

添加拦截器
为了实现更灵活的控制(如添加全局 token、处理错误状态码),可以为请求库增加拦截器机制。

// 定义拦截器接口
interface Interceptor<T> {
  onFulfilled?: (value: T) => T | Promise<T>;
  onRejected?: (error: any) => any;
}

// 拦截器管理类
class InterceptorManager<T> {
  private interceptors: Array<Interceptor<T>>;
  constructor() { this.interceptors = []; }
  use(interceptor: Interceptor<T>) { this.interceptors.push(interceptor); }
  forEach(fn: (interceptor: Interceptor<T>) => void) {
    this.interceptors.forEach((interceptor) => { if (interceptor) fn(interceptor); });
  }
}

// 增强版的 request 函数
export async function request<T>(url: string, options: IRequestOptions = {}): Promise<T> {
  const requestInterceptors = new InterceptorManager<IRequestOptions>();
  const responseInterceptors = new InterceptorManager<any>();

  // 示例:添加请求拦截器(如添加认证Token)
  requestInterceptors.use({
    onFulfilled: (config) => {
      const token = localStorage.getItem('authToken');
      if (token) {
        config.headers = { ...config.headers, Authorization: `Bearer ${token}` };
      }
      return config;
    },
  });

  // 示例:添加响应拦截器(如统一处理错误)
  responseInterceptors.use({
    onFulfilled: (response) => {
      // 可以在这里对响应数据进行预处理
      return response;
    },
    onRejected: (error) => {
      console.error('请求出错:', error);
      // 可以在这里统一弹出错误提示
      throw error;
    },
  });

  // 执行请求拦截器
  requestInterceptors.forEach(async (interceptor) => {
    options = (await interceptor.onFulfilled?.(options)) ?? options;
  });

  let response = await fetch(url, options);

  // 执行响应拦截器
  responseInterceptors.forEach((interceptor) => {
    response = interceptor.onFulfilled?.(response) ?? response;
  });

  if (!response.ok) {
    // 触发响应拦截器的错误处理
    responseInterceptors.forEach((interceptor) => {
      interceptor.onRejected?.(new Error(`HTTP ${response.status}`));
    });
    throw new Error(`Request failed with status code ${response.status}`);
  }

  return response.json() as Promise<T>;
}

在业务代码中使用

// api/feedback.ts
import { request } from '@/utils/request';
interface ParamsType { /* ... */ }
interface ResType { code: number; message: string; }

export async function feedbackSubmit(params: ParamsType): Promise<ResType> {
  const data: ResType = await request('https://api.example.com/feedback', {
    method: 'POST',
    body: JSON.stringify(params),
  });
  return data;
}

// 组件中调用
feedbackSubmit(formData)
  .then((res) => {
    if (res.code === 0) {
      message.success('提交成功!');
    } else {
      message.error(res.message);
    }
  })
  .catch(() => {
    message.error('网络请求失败');
  });

18. 跨域问题与代理配置详解

18-1 开发环境:使用 Vite 代理

在本地开发时,由于前端服务(如 localhost:5173)与后端 API 地址不同源,会产生跨域问题。Vite 提供了内置的代理功能来解决此问题。

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      // 将本地请求 /api 代理到目标服务器
      '/api': {
        target: 'http://api.example.com',
        changeOrigin: true, // 修改请求头中的Origin为目标地址
        rewrite: (path) => path.replace(/^\/api/, ''), // 可重写请求路径
      },
    },
  },
});

配置后,在代码中请求 /api/user,Vite 开发服务器会将其代理到 http://api.example.com/user,从而绕过浏览器的同源限制。

18-2 生产环境:Nginx 反向代理或后端配置 CORS

项目上线后,解决方法取决于部署方式:

  1. 前端与后端同域部署:将前端构建产物放到后端服务的静态目录,此时不存在跨域。
  2. 前后端分离部署
    • 方案A(推荐)后端配置 CORS (Cross-Origin Resource Sharing),在响应头中添加 Access-Control-Allow-Origin 等字段,明确允许前端的域名进行跨域访问。
    • 方案B使用 Nginx 反向代理。将前端和后端 API 都通过同一个域名和端口访问,由 Nginx 根据路径将 API 请求转发到真实的后端服务器。
# Nginx 配置示例
server {
    listen 80;
    server_name yourdomain.com;

    location / {
        root /path/to/your/frontend/dist;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://backend-server:3000/; # 转发到后端服务
        proxy_set_header Host $host;
    }
}

18-3 区分环境动态设置请求地址

在代码中,需要根据当前是开发环境还是生产环境,来决定使用代理路径还是真实 API 地址。

// api/feedback.ts
export async function feedbackSubmit(params: ParamsType): Promise<ResType> {
  let baseUrl = '';
  if (process.env.NODE_ENV === 'development') {
    baseUrl = '/api'; // 开发环境走Vite代理
  } else {
    baseUrl = 'https://api.yourdomain.com'; // 生产环境用真实地址
  }
  const data: ResType = await request(`${baseUrl}/feedback`, {
    method: 'POST',
    body: JSON.stringify(params),
  });
  return data;
}

19. 环境变量在前端项目中的管理与使用

环境变量是配置应用在不同环境(开发、测试、生产)中行为的关键。

19-1 概念与作用

环境变量是一组键值对,用于存储与部署环境相关的配置,如 API 地址、功能开关等。使用环境变量可以避免将硬编码的配置写入代码,提高应用的可移植性和安全性。

19-2 在 Vite 中的使用

Vite 内置了环境变量支持。项目根目录下的 .env.development.env.production 等文件可以定义环境变量。在代码中,可以通过 import.meta.env 对象访问。

19-3 使用 cross-env 设置构建时变量

有时我们需要在 package.json 的脚本命令中传递环境变量,例如指定构建目标环境。cross-env 可以跨平台地设置环境变量。

  1. 安装pnpm install cross-env
  2. 配置 package.json
    {
      "scripts": {
        "dev": "cross-env ENV=dev vite",
        "build:dev": "cross-env ENV=dev vite build",
        "build:prod": "cross-env ENV=prod vite build"
      }
    }
  3. 在代码或配置文件中使用

    // 在vite.config.ts中
    const env = process.env.ENV; // 'dev' 或 'prod'
    
    // 在业务代码中,通过Vite的import.meta.env访问
    if (import.meta.env.MODE === 'development') {
      // 开发环境逻辑
    }

注意process.env 是 Node.js 环境下的对象。在浏览器中直接访问 process 会报错。但是,像 Vite、Webpack 这样的构建工具,会在构建过程中将 process.env.XXX 替换为具体的值(“静态替换”),或者将变量注入到 import.meta.env 中。因此,在前端代码中我们通常使用 import.meta.env

20. 使用 vite-plugin-html 动态注入页面元信息

为了优化 SEO,页面的 <title><meta name=”description”> 等标签内容可能需要根据后台数据动态生成。vite-plugin-html 插件可以在构建时向 HTML 模板注入内容。

1. 安装插件及 node-fetch(用于在 Node 环境调用接口):

pnpm install vite-plugin-html -D
pnpm install node-fetch -S

2. 在 vite.config.ts 中配置

import { createHtmlPlugin } from 'vite-plugin-html';
import fetch from 'node-fetch';
(global as any).fetch = fetch; // 让Node环境支持fetch

// 定义接口返回类型
interface IHtmlHeadContent {
  seo: {
    title: string;
    description: string;
    keywords: string;
  };
}

// 异步函数获取SEO数据
async function getHtmlHeadContent(): Promise<IHtmlHeadContent> {
  const url = process.env.NODE_ENV === 'development'
    ? 'https://dev.yourdomain.com/seo-data.json'
    : 'https://yourdomain.com/seo-data.json';
  const response = await fetch(url);
  const data = await response.json();
  return data as IHtmlHeadContent;
}

export default defineConfig(async () => {
  const seoData = await getHtmlHeadContent();
  return {
    plugins: [
      react(),
      createHtmlPlugin({
        minify: true,
        inject: {
          data: {
            title: seoData.seo.title,
            description: seoData.seo.description,
            keywords: seoData.seo.keywords,
          },
        },
      }),
    ],
  };
});

3. 在 index.html 中使用 EJS 语法获取注入的数据

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title><%- title %></title>
    <meta name="description" content="<%= description %>" />
    <meta name="keywords" content="<%= keywords %>" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

构建时,插件会自动获取接口数据,并将其替换到 HTML 模板的对应位置。

21. 使用 Ant Design Form 实例进行表单操作

当使用 Ant Design 的 Form 组件时,表单项(如 InputTextArea)会变为受控组件。此时,不能直接通过 ref 去操作其 DOM 来设置值,而应该使用 Form.useForm 创建的 form 实例。

1. 获取并绑定 form 实例

import { Form, Input } from 'antd';
const { TextArea } = Input;

const MyFormComponent = () => {
  // 创建Form实例
  const [formInstance] = Form.useForm();

  // 在需要时(如数据回显)设置表单值
  const setInitialValues = () => {
    formInstance.setFieldsValue({
      emailTitle: '初始标题',
      emailContent: '初始内容',
    });
  };

  return (
    <Form form={formInstance} layout="vertical">
      <Form.Item label="邮件主题" name="emailTitle" rules={[{ required: true }]}>
        <Input placeholder="请输入主题" />
      </Form.Item>
      <Form.Item label="邮件正文" name="emailContent">
        <TextArea placeholder="请输入正文" />
      </Form.Item>
    </Form>
  );
};

2. 动态增减表单项(Form.List)的值管理
对于 Form.List,同样可以通过 form 实例的 getFieldValuesetFieldsValue 来读取和设置其值。

const [formInstance] = Form.useForm();

// 设置Form.List的初始值
formInstance.setFieldsValue({
  welfareType: ['福利A', '福利B'], // welfareType 是 Form.List 的 name
});

// 在 Form.List 的渲染函数内获取当前值
<Form.List name="welfareType">
  {(fields, { add, remove }) => {
    // 获取当前列表的值
    const currentList = formInstance.getFieldValue('welfareType') || [];
    return fields.map((field, index) => (
      <Form.Item {...field} key={field.key}>
        <Input
          value={currentList[index]} // 绑定值
          onChange={(e) => {
            // 更新逻辑...
          }}
        />
      </Form.Item>
    ));
  }}
</Form.List>

使用 form 实例提供的方法(setFieldsValue, getFieldValue, validateFields 等)是操作 Ant Design 表单的标准且推荐的方式。

总结

本文涵盖了从日常开发细节到项目工程化配置的多个实用主题。无论是处理常见的 npm 依赖问题、优化 CSS 与布局、管理项目配置,还是实现国际化、封装请求等高级功能,希望这些经验总结能为你的前端开发之旅提供有价值的参考。在实践中不断积累和优化这些“小知识”,是构建稳健、可维护大型应用的基础。欢迎在 云栈社区 分享与讨论你的前端开发心得。




上一篇:emoveWindowsAI:把 Windows 10/11 里的 Copilot、Recall 等 AI 组件“关掉并清掉”的 PowerShell 脚本
下一篇:货拉拉星图平台高并发架构优化:从5秒到100ms的性能跃迁实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-16 19:34 , Processed in 0.237152 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表