深入掌握Environments环境配置与实战技巧

打工人洛熙 工具 阅读 1,343
赞 21 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月接手了个老项目重构,前端这块原本是用一堆全局变量加 jQuery 撸出来的,环境配置全写在 HTML 里,不同环境靠手动改 script 标签引入的 config.js。每次上线前运维都得盯着我,生怕我发错了测试配置到生产。

深入掌握Environments环境配置与实战技巧

实在受不了了,就想着干脆把环境管理这块重做一下。本来想直接上 Webpack DefinePlugin 硬编码,但后来一想,这项目以后可能要支持多租户部署,配置项会越来越多,硬编码肯定不行。最后决定整一个基于 JSON 配置 + 运行时加载的 environment 管理方案。

核心思路:运行时动态加载环境配置

我的想法很简单:页面打开时先异步拉取一个 env.json,根据域名自动判断当前环境,然后把所有配置挂载到 window.APP_CONFIG 上。这样打包只需要一份代码,部署时只要换掉 env.json 就行。

实现起来也不复杂,入口 index.html 里加了个小脚本:

<script>
  // 先定义个默认配置兜底
  window.APP_CONFIG = {
    API_URL: 'https://jztheme.com/api',
    ENV: 'development',
    DEBUG: true
  };

  // 动态加载环境配置
  fetch('/env.json')
    .then(res => res.json())
    .then(config => {
      window.APP_CONFIG = { ...window.APP_CONFIG, ...config };
      // 加载完再启动 Vue 应用
      startApp();
    })
    .catch(err => {
      console.warn('加载 env.json 失败,使用默认配置', err);
      startApp(); // 出错也得启动,不能卡死
    });

  function startApp() {
    // 这里才是真正的应用入口
    import('./main.js');
  }
</script>

最大的坑:首屏请求全挂了

理想很丰满,现实很骨感。改完上线第一天,监控报警炸了——大量接口 404。查了半天才发现,因为 fetch env.json 是异步的,而有些组件在 startApp 前就发请求了,结果读到的是默认配置里的测试地址,根本不对。

开始没想到这个问题,以为 import(‘./main.js’) 能完全延迟执行。但实际上,webpack 的 dynamic import 只是“告诉”它什么时候下载模块,模块内部的副作用(比如 api.js 里直接使用的 window.APP_CONFIG.API_URL)会在模块加载时立刻执行。

也就是说,我还没来得及替换 window.APP_CONFIG,人家就已经拿着旧配置去发请求了。

折腾了半天发现,这种运行时动态配置的核心矛盾在于:你必须确保所有依赖配置的代码都在配置加载完成后才执行。

最终的解决方案:搞个配置等待队列

后来参考了以前做过的一个 SSR 项目,加了个简单的 Promise 锁机制。所有需要配置的地方,先等一等。

// config-loader.js
let resolveReady;
let rejectReady;
const readyPromise = new Promise((resolve, reject) => {
  resolveReady = resolve;
  rejectReady = reject;
});

export const Config = {
  current: null,

  async load() {
    try {
      const res = await fetch('/env.json');
      const config = await res.json();
      this.current = {
        API_URL: 'https://jztheme.com/api',
        ENV: 'development',
        DEBUG: false,
        ...config
      };
      resolveReady(this.current);
    } catch (err) {
      console.warn('加载配置失败,使用默认值');
      this.current = window.APP_CONFIG; // 回退到初始默认
      resolveReady(this.current);
    }
  },

  // 所有需要配置的地方都要 await Config.ready()
  ready() {
    return readyPromise;
  }
};

// 自动触发加载
Config.load();

然后在 api.js 里这么用:

import { Config } from './config-loader';

let API;

// 不再直接使用配置,而是包装一层初始化函数
export async function initAPI() {
  const config = await Config.ready();
  API = {
    fetchUsers: () => fetch(${config.API_URL}/users),
    createUser: (data) => fetch(${config.API_URL}/users, {
      method: 'POST',
      body: JSON.stringify(data)
    })
  };
  return API;
}

// 导出一个带锁的调用方式
export async function getAPI() {
  if (!API) await initAPI();
  return API;
}

组件里就得改成:

import { getAPI } from './api';

async mounted() {
  const API = await getAPI();
  const res = await API.fetchUsers();
  this.users = await res.json();
}

这里注意我踩过好几次坑

  • 一开始想偷懒,在 main.js 顶部 await Config.ready(),结果 webpack 把所有 import 提前执行了,照样出事。
  • 有人提议用 Event Loop 微任务 hack,比如 setTimeout(() => startApp(), 0),但这不靠谱,没法保证顺序。
  • 最后这个 Promise 锁虽然多写了点代码,但逻辑清晰,至少没再出问题。

还有些小问题没完美解决

现在每次调用 getAPI() 都要 await,写起来有点烦。理论上可以做个单例缓存,但怕异步竞争,暂时就这样了。反正影响不大,最多就是多几个 await。

另外 env.json 缓存问题也没处理。正常情况下 Nginx 配了 no-cache,但如果用户网络差或者离线访问,还是会用浏览器缓存的老配置。不过我们内部系统,刷新一下就行,懒得加版本号或时间戳了。

回顾与反思

这套方案上线两周了,目前稳定。最大的好处是部署真的变简单了,CI/CD 流程里不再需要根据不同环境打包,只传一个 env.json 就行。

缺点也很明显:首屏慢了一丢丢,因为要等配置加载完才能启动应用。实测平均多耗时 120ms 左右,在可接受范围。

如果下次再做类似需求,可能会考虑预埋多套配置在 JS 里,通过域名自动切换,牺牲一点包体积换来更稳定的启动流程。但现在这套也能跑,就不动了。

以上是我的项目经验,希望对你有帮助

这种 environment 管理看似简单,真动手就会踩各种异步陷阱。特别是老项目改造,更要小心副作用执行时机。我这个方案不是最优的,但够用、可控、容易理解。

如果你有更好的做法,比如用 Module Federation 或者其他 loader 机制,欢迎评论区交流。这个坑我不想再踩第二次了。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论