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)
将主题逻辑封装为模块,直接在控制器中导入使用,彻底规避竞态问题:
- 创建
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; }
- 在
application.js中保留对window.theme的设置(如果其他组件需要使用):
import { getCurrentTheme } from './theme'; function setWindowTheme() { window.theme = getCurrentTheme(); } document.addEventListener('DOMContentLoaded', setWindowTheme); document.addEventListener('turbo:load', setWindowTheme);
- 修改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
相关产品推荐
相关产品推荐

