普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月20日首页

Vite 和 Wepack 中如何处理环境变量

作者 乘方
2026年3月19日 22:15

环境变量文件

.env: 所有环境都会加载 .env.local: 所有环境都会加载,但被 git 忽略 .env.[mode]: 只在指定模式下加载(如 .env.development、.env.production) .env.[mode].local: 只在指定模式下加载,且被 git 忽略

备注:后续加载的文件变量会覆盖前面的

在 Webpack 工程中

步骤

  1. 通过cross env配置脚本指定运行的环境

    cross-env NODE_ENV=development

  2. 在node环境中使用process.env.NODE_ENV来获取环境参数mode.

  3. 使用dotenv读取项目根目录下的对应.env.[mode]文件,解析其中的键值对,并将其挂载到node环境下的process.env对象上,之后就可以通过process.env.VAR_NAME在node中访问它们。

  4. 将读取到的环境变量注入业务代码,作为全局变量

    1. 通过 new Webpack.DefinePlugin 直接定义
    2. 使用 dotenv 加载 .env 文件,再通过 dotenv-webpack 插件注入

在 Vite 工程中

步骤

  1. 默认运行vite是开发环境 --mode development,vite build是运行生产环境 --mode production. 如vite --mode test,指定测试环境,对应.env.test文件

    vite中的mode指的是环境参数,而webpack中的mode指的是打包方式

  2. 环境参数可以从defineConfig回调函数中的config参数获取

  3. 在配置文件中想要获取.env文件中的变量,需要使用vite自带的loadEnv来加载

    import { defineConfig, loadEnv } from "vite";
    
    export default defineConfig(({ command, mode }) => {
      // 加载环境变量
      const env = loadEnv(mode, process.cwd(), "");
      // 现在env中包含所有环境变量,包括没有前缀的
      // 如果需要只获取VITE_前缀的,可以省略第三个参数或指定'VITE_'
      // 如果希望所有变量都可用,第三个参数传''(空字符串)
    
      // 可以在配置中使用env
      return {
        // 比如设置base
        base: env.VITE_BASE_URL || "/",
        // 或者通过define注入更多变量
        define: {
          __APP_VERSION__: JSON.stringify(env.APP_VERSION),
        },
      };
    });
    
  4. 在任何客户端代码(.js、.jsx、.ts、.vue、.svelte 等)中,通过import.meta.env对象访问这些变量,无需手动添加。

    • Vite 默认只暴露VITE_开头的变量,这是一种安全机制。如果你确实需要将其他变量暴露给客户端,可以使用define插件手动注入
    • 还包含一些内置变量也会自动注入到客户端页面:

      MODE:当前运行模式(development / production 等) BASE_URL:应用部署的基础路径(由 base 配置项决定) PROD:是否为生产环境(布尔值) DEV:是否为开发环境(布尔值) SSR:是否为服务端渲染(布尔值)

import.meta[]

是一个给 JavaScript 模块暴露特定上下文元数据的全局对象,包含了当前模块的信息,比如模块的 URL 。它包含哪些具体属性,取决于代码运行的环境(如浏览器、Node.js、Bun 或 Nuxt 框架)。

1. import.meta.url:获取当前模块的URL, 定位模块本身的位置。

// 假设文件地址为:/projects/my-app/src/utils.js
console.log(import.meta.url);
// 浏览器环境输出: http://localhost:3000/src/utils.js
// Node.js 环境输出: file:///projects/my-app/src/utils.js

结合new URL加载资源:这是处理静态资源路径的推荐方式,能保证路径总是正确的

2. import.meta.resolve:解析相对路径,基于当前模块的URL来解析其他模块或文件的路径,特别适合在Node环境中替代__dirname使用。

3. import.meta.hot:实现热模块替换 (HMR),在开发模式下,可以利用它来实现模块热替换,提升开发效率。

// 使用 Pinia 状态管理库时的 HMR 示例
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // 当模块更新时,执行一些操作,比如重新应用状态
    console.log("模块已热替换", newModule);
  });
}

4. import.meta.glob: 提供一个路径模式,构建工具(Vite)会在编译时静态分析,找到所有匹配的文件,并返回一个方便你操作的对象。

const modules = import.meta.glob('./dir/\*.js', { eager: true }) eager=true,返回模块为懒加载模式

4. import.meta.env: 可以方便地获取进程的环境变量。

备注import.meta.env import.meta.glob import.meta.hot,是客户端专属API,不支持在node环境下访问。

useEffect 中执行定时器引发的闭包问题

作者 乘方
2026年3月19日 21:50

关于定时器

  • 定时器被清理后,未完成的定时任务就不会触发
  • 定时器执行完任务后,会自动销毁
  • 定时器制造内存泄漏的原因是:组件卸载后,存在未触发定时任务还没执行(回调任务还存在浏览器的任务队列里面),因为闭包的存在,定时器引用的外界变量不会销毁。

问题一

想实现的效果:两秒后输出点击按钮的次数

// 问题代码
export default function Test() {
  const [n, setN] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      console.log(n);
    }, 2000);
  });

  return (
    <div>
      <h1>{n}</h1>
      <button
        onClick={() => {
          setN((prevN) => prevN + 1);
        }}
      >
        Click
      </button>
    </div>
  );
}

errorLog.png

结果:打开页面连续点击 2 次按钮,两秒后(点击的时间忽略),控制台连续输出 0 1 2

错误原因

  1. 初始化第 1 次执行 useEffect,开启定时任务 timer1 (两秒后输出 n)
  2. 第 1 次点击,触发 setN,n 被改变页面重新渲染,重新执行函数组件 Test,第 2 次触发 useEffect。开启定时任务 timer2 (两秒后输出 n)。
  3. 第 2 次点击,第 3 次触发 useEffect。开启定时任务 timer3 (两秒后输出 n)。停止点击。
  4. useEffect 的执行时机是在页面绘制后(此时 n 值早已是最新的值),每次函数组件执行可以看作一次闭包,定时器里引用当前上下文中的 n 值。
  5. 三个定时器大约两秒后依次触发,输出对应闭包下的 n 值;

解决办法: 在两秒内连续点击,及时清理上次的定时器,类似于 防抖

useEffect(() => {
  const timer = setTimeout(() => {
    console.log(n);
  }, 2000);
  return () => clearTimeout(timer);
});

rightLog.png

问题二

想实现的效果:倒计时抢券功能,5 秒后,提示 "活动结束"。

// 问题代码
export default function Test() {
  const [n, setN] = useState(5);

  useEffect(() => {
    const timer = setInterval(() => {
      setN(n - 1);
      console.log(n);
      if (n === 0) {
        clearInterval(timer);
      }
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  });

  return (
    <div>
      <h1>{n || "活动结束"}</h1>
    </div>
  );
}

errorLog2.png

结果:倒计时结束定时器并没有停下来,并且打印的值始终比实际值延后。

错误原因

  1. 定时器不会停止:destroy 清理的上次的定时器,即便 n = 0 清理的是当前定时器,setN 触发更新还是会创建新的定时器,并打印对应闭包中的 n 值。

    可以做判断 n !== 0 && setN(n -1) : clearInterval(timer) 实现效果,但还是需要开多个定时器,不是好的解决办法

  2. 值延后打印:1s 后执行定时器回调,打印的 n 还是引用先前闭包的值,此时还触发了 setN,页面重新渲染 n = n - 1,造成页面刷新和控制台打印看起来在同一时间,但数值不一样。

解决办法: 让定时器具有唯一性;使用 dispath 的回调函数方式获取最新值 setN(prevN => prevN + 1)

useEffect(() => {
  const timer = setInterval(() => {
    setN((prevN) => {
      const next = prevN - 1;
      console.log({ n1: n, n2: next });
      !next && clearInterval(timer);
      return next;
    });
  }, 1000);
  return () => {
    clearInterval(timer);
  };
}, []);

rightLog2.png

因为这个唯一定时器是初始化创建的,n1 一直引用的是第一次闭包里的 n 值,因此一直输出 5。

那 n2 为什么就能获取最新的值呢?

当调用 setN(prevN => ...) 时,React 不会立即执行这个函数,而是将它加入到一个更新队列中。在后续的渲染阶段,React 会按顺序处理队列中的每个更新函数,并传入当前已经应用了前面所有更新后的状态值作为参数。在定时器这类异步场景中,回调函数定义时的 n 可能早已过时。但函数式更新让回调不再依赖外部闭包变量,而是依赖 React 内部管理的实时状态,因此总能拿到最新值。

*使用 useRef 来 "绕过" 闭包

原理:

每次渲染时,函数组件内的局部变量(如 state)都会被重新创建,并被当前渲染闭包捕获。 而 ref 对象在组件的整个生命周期内保持不变,在多次渲染间共享,它的 .current 属性可以随时修改且不会触发重新渲染。 因此,我们可以在每次渲染时,将最新的 state 同步到 ref.current 中,然后在定时器回调里通过 ref.current 获取最新值,而不再依赖闭包中捕获的旧值。

import { useState, useEffect, useRef } from "react";

export default function Test() {
  const [n, setN] = useState(5);
  const nRef = useRef(n); // 创建一个 ref,初始值为 n

  // 每次渲染后,将最新的 n 同步到 ref 中
  useEffect(() => {
    nRef.current = n;
  });

  useEffect(() => {
    const timer = setInterval(() => {
      // 通过 ref.current 获取最新的 n
      const currentN = nRef.current;
      console.log("当前 n:", currentN);
      if (currentN > 0) {
        setN(currentN - 1);
      } else {
        clearInterval(timer);
      }
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 空依赖数组,确保定时器只创建一次

  return <h1>{n || "活动结束"}</h1>;
}

备注: 这是一种通用技巧,不仅适用于定时器,也适用于任何需要绕过闭包陷阱的异步操作(如事件监听、requestAnimationFrame 等)。

❌
❌