You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails7+Bootstrap项目中Stimulus控制器无法读取window.theme

解决Rails 7 Stimulus控制器中window.theme未定义的竞态问题

问题原因

你遇到的确实是竞态条件:Stimulus控制器的connect方法触发时,挂载window.theme的代码还未完成执行(或者DOM中的link标签还未被正确遍历识别),导致控制器内访问window.theme时为undefined。

具体来说,挂载theme的代码直接遍历document.querySelectorAll("link[href]"),如果这段代码在DOM未完全加载完成时执行,或者Stimulus控制器因Turbo的渐进式加载先完成初始化,就会出现window.theme未被赋值的情况。

解决方案

方案1:确保theme挂载代码在DOM/Turbo加载完成后执行

修改挂载theme的代码,将其包裹在DOM加载完成事件和Turbo页面加载事件中,保证在所有link标签都加载后再赋值window.theme:

/*
 * Add color theme colors to the window object
 * so this can be used by the charts and vector maps
 */

const lightTheme = {
  "id": "light",
  "name": "Light",
  "primary": "#3B7DDD",
  "secondary": "#6c757d",
  "success": "#1cbb8c",
  "info": "#17a2b8",
  "warning": "#fcb92c",
  "danger": "#dc3545",
  "white": "#fff",
  "gray-100": "#f8f9fa",
  "gray-200": "#e9ecef",
  "gray-300": "#dee2e6",
  "gray-400": "#ced4da",
  "gray-500": "#adb5bd",
  "gray-600": "#6c757d",
  "gray-700": "#495057",
  "gray-800": "#343a40",
  "gray-900": "#212529",
  "black": "#000"
};

const darkTheme = {
  "id": "dark",
  "name": "Dark",
  "primary": "#3B7DDD",
  "secondary": "#7a828a",
  "success": "#1cbb8c",
  "info": "#17a2b8",
  "warning": "#fcb92c",
  "danger": "#dc3545",
  "white": "#222E3C",
  "gray-100": "#384350",
  "gray-200": "#4e5863",
  "gray-300": "#646d77",
  "gray-400": "#7a828a",
  "gray-500": "#91979e",
  "gray-600": "#a7abb1",
  "gray-700": "#bdc0c5",
  "gray-800": "#d3d5d8",
  "gray-900": "#e9eaec",
  "black": "#fff"
}

// 封装主题设置逻辑
function setWindowTheme() {
  document.querySelectorAll("link[href]").forEach((link) => {
    const href = link.href.split("/");
    if(href.pop() === "dark.css"){
      window.theme = darkTheme;
    } else {
      window.theme = lightTheme;
    }
  });
}

// 原生DOM加载完成时执行
document.addEventListener('DOMContentLoaded', setWindowTheme);
// Turbo页面切换时执行(适配Rails 7的Turbo特性)
document.addEventListener('turbo:load', setWindowTheme);

方案2:在Stimulus控制器中等待theme加载完成

如果不想修改挂载代码,可以在控制器内添加等待逻辑,直到window.theme存在后再初始化地图:

import { Controller } from "@hotwired/stimulus"
import "../../modules/vector-maps/world"

export default class extends Controller {
  static targets = ["targetMap"]

  constructor() {
    super();
    this.map = {};
  }

  connect() {
    this.waitForThemeThenInitMap();
  }

  mapResize(event) {
    this.map.updateSize();
  }

  waitForThemeThenInitMap() {
    if (window.theme) {
      this.InitMap();
    } else {
      // 每50ms检查一次,直到theme存在
      setTimeout(() => this.waitForThemeThenInitMap(), 50);
    }
  }

  InitMap() {
    var markers = [{
      coords: [37.77, -122.41],
      name: "San Francisco: 375"
    },
    {
      coords: [40.71, -74.00],
      name: "New York: 350"
    },
    {
      coords: [39.09, -94.57],
      name: "Kansas City: 250"
    },
    {
      coords: [36.16, -115.13],
      name: "Las Vegas: 275"
    },
    {
      coords: [32.77, -96.79],
      name: "Dallas: 225"
    }
    ];

    console.log('theme object attached to window: ', window.theme)

    this.map = new jsVectorMap({
      map: "world",
      selector: "#world_map",
      zoomButtons: true,
      markers: markers,
      markerStyle: {
        initial: {
          r: 9,
          stroke: window.theme.white,
          strokeWidth: 7,
          stokeOpacity: .4,
          fill: '#3B7DDD'
        },
        hover: {
          fill: '#3B7DDD',
          stroke: '#3B7DDD'
        }
      },
      regionStyle: {
        initial: {
          fill: '#e9ecef'
        }
      },
      zoomOnScroll: false
    });
  }
}

方案3:模块化导入主题(最可靠,避免依赖window)

将主题逻辑封装为模块,直接在控制器中导入使用,彻底规避竞态问题:

  1. 创建app/javascript/theme.js文件:
export const lightTheme = {
  "id": "light",
  "name": "Light",
  "primary": "#3B7DDD",
  "secondary": "#6c757d",
  "success": "#1cbb8c",
  "info": "#17a2b8",
  "warning": "#fcb92c",
  "danger": "#dc3545",
  "white": "#fff",
  "gray-100": "#f8f9fa",
  "gray-200": "#e9ecef",
  "gray-300": "#dee2e6",
  "gray-400": "#ced4da",
  "gray-500": "#adb5bd",
  "gray-600": "#6c757d",
  "gray-700": "#495057",
  "gray-800": "#343a40",
  "gray-900": "#212529",
  "black": "#000"
};

export const darkTheme = {
  "id": "dark",
  "name": "Dark",
  "primary": "#3B7DDD",
  "secondary": "#7a828a",
  "success": "#1cbb8c",
  "info": "#17a2b8",
  "warning": "#fcb92c",
  "danger": "#dc3545",
  "white": "#222E3C",
  "gray-100": "#384350",
  "gray-200": "#4e5863",
  "gray-300": "#646d77",
  "gray-400": "#7a828a",
  "gray-500": "#91979e",
  "gray-600": "#a7abb1",
  "gray-700": "#bdc0c5",
  "gray-800": "#d3d5d8",
  "gray-900": "#e9eaec",
  "black": "#fff"
};

// 获取当前主题的方法
export function getCurrentTheme() {
  // 检查是否存在dark.css的link标签
  const hasDarkTheme = document.querySelector('link[href$="dark.css"]') !== null;
  return hasDarkTheme ? darkTheme : lightTheme;
}
  1. 在application.js中保留对window.theme的设置(如果其他组件需要使用):
import { getCurrentTheme } from './theme';

function setWindowTheme() {
  window.theme = getCurrentTheme();
}

document.addEventListener('DOMContentLoaded', setWindowTheme);
document.addEventListener('turbo:load', setWindowTheme);
  1. 修改Stimulus控制器,直接导入主题方法:
import { Controller } from "@hotwired/stimulus"
import "../../modules/vector-maps/world"
import { getCurrentTheme } from "../theme" // 路径根据实际文件位置调整

export default class extends Controller {
  static targets = ["targetMap"]

  constructor() {
    super();
    this.map = {};
  }

  connect() {
    this.InitMap();
  }

  mapResize(event) {
    this.map.updateSize();
  }

  InitMap() {
    // 直接获取当前主题,无需依赖window
    const theme = getCurrentTheme();

    var markers = [{
      coords: [37.77, -122.41],
      name: "San Francisco: 375"
    },
    {
      coords: [40.71, -74.00],
      name: "New York: 350"
    },
    {
      coords: [39.09, -94.57],
      name: "Kansas City: 250"
    },
    {
      coords: [36.16, -115.13],
      name: "Las Vegas: 275"
    },
    {
      coords: [32.77, -96.79],
      name: "Dallas: 225"
    }
    ];

    console.log('current theme: ', theme)

    this.map = new jsVectorMap({
      map: "world",
      selector: "#world_map",
      zoomButtons: true,
      markers: markers,
      markerStyle: {
        initial: {
          r: 9,
          stroke: theme.white,
          strokeWidth: 7,
          stokeOpacity: .4,
          fill: '#3B7DDD'
        },
        hover: {
          fill: '#3B7DDD',
          stroke: '#3B7DDD'
        }
      },
      regionStyle: {
        initial: {
          fill: '#e9ecef'
        }
      },
      zoomOnScroll: false
    });
  }
}

方案选择

  • 方案1适合需要保持window.theme全局可用的场景,只需修改挂载代码即可。
  • 方案2是快速修复,无需改动原有挂载逻辑,但依赖轮询检查,不够优雅。
  • 方案3是最健壮的方式,符合模块化开发规范,彻底消除竞态问题,推荐优先使用。

内容的提问来源于stack exchange,提问作者Augusto Samamé Barrientos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 11:12:01